diff options
| author | Maria Lisina <sekoohaka.sarisan@gmail.com> | 2026-08-10 10:38:14 +0500 |
|---|---|---|
| committer | Maria Lisina <sekoohaka.sarisan@gmail.com> | 2026-08-10 10:38:14 +0500 |
| commit | 6dc1149d078fc3b04a19696bfb7d5d6940372175 (patch) | |
| tree | 169f7f71e6a292a4ab61a0493000e1add4430f8e | |
| parent | 6836426ae183b9a7d0ec99c1e147e8ad6edabd92 (diff) | |
| parent | 80517840f4ae64469721598e8373d81f2e4493dd (diff) | |
Merge tag '6.0.0' of https://repo.dec05eba.com/gpu-screen-recorder into debian
110 files changed, 11078 insertions, 5673 deletions
@@ -30,3 +30,7 @@ gsr-kms-server *.jpg *.jpeg *.png + +subprojects/* +!subprojects/*.wrap +!subprojects/packagefiles/ @@ -43,14 +43,11 @@ If you still prefer to install GPU Screen Recorder with a package manager instea Here are some known unofficial packages: * Alpine Linux: [gpu-screen-recorder](https://pkgs.alpinelinux.org/package/edge/testing/x86/gpu-screen-recorder) * AppImage [AppImage GitHub releases](https://github.com/pkgforge-dev/gpu-screen-recorder-AppImage/releases) -* CentOS: [Copr](https://copr.fedorainfracloud.org/coprs/brycensranch/gpu-screen-recorder-git/) * Debian: [gpu-screen-recorder-cli](https://tracker.debian.org/pkg/gpu-screen-recorder) or [gpu-screen-recorder alternative repository](https://sekoohaka.ddns.net/?p=about#debian-repository) -* Fedora: [Copr](https://copr.fedorainfracloud.org/coprs/brycensranch/gpu-screen-recorder-git/) * Nix: [NixOS wiki](https://wiki.nixos.org/wiki/Gpu-screen-recorder) * Nobara: [Nobara wiki](https://wiki.nobaraproject.org/en/general-usage/additional-software/GPU-Screen-Recorder) * OpenMandriva: [gpu-screen-recorder](https://github.com/OpenMandrivaAssociation/gpu-screen-recorder) * openSUSE: [openSUSE software repository](https://software.opensuse.org/package/gpu-screen-recorder) -* Solus: [gpu-screen-recorder](https://github.com/getsolus/packages/tree/main/packages/g/gpu-screen-recorder) * Ubuntu: [gpu-screen-recorder-cli](https://launchpad.net/ubuntu/+source/gpu-screen-recorder) * Void Linux: [gpu-screen-recorder](https://github.com/cherrybtw/gsr4xbps) @@ -75,6 +72,16 @@ When building GPU Screen Recorder with portal support (`-Dportal=true` meson opt * libdbus * libpipewire (and libspa which is usually part of libpipewire) +## Building with a statically linked ffmpeg +The `-Dffmpeg_static=true` meson option (disabled by default) downloads ffmpeg and the libraries that ffmpeg depends on (x264, opus, mbedtls, srt and nv-codec-headers), builds them from source with lto and only the components that GPU Screen Recorder uses and then links them statically into GPU Screen Recorder. +This removes the runtime dependency on the system ffmpeg and on all of those libraries. `make`, `nasm`, `cmake` and `python3` are needed to build them and the first `meson setup` takes a few minutes longer because of it. + +mbedtls is used as the tls backend instead of openssl because it's much smaller. mbedtls has no built-in default certificate location, so ffmpeg is patched to look for the certificate store of the system (and to honor the `SSL_CERT_FILE` and `SSL_CERT_DIR` environment variables) the same way that openssl does. + +The only libraries that ffmpeg is still dynamically linked to are libva, libdrm (which GPU Screen Recorder also uses directly) and the c/c++ runtime. + +This uses the same ffmpeg version as the GPU Screen Recorder flatpak, including the patch that makes nvenc negotiate the NVENC API version at runtime instead of at build time, which makes nvidia video encoding work on drivers down to 470.57.02 (Kepler) while still supporting AV1 and the RTX 5000 series. + ## Runtime dependencies * libglvnd (which provides libgl, libglx and libegl) is needed. Your system needs to support at least OpenGL ES 3.0 (released in 2012) * vulkan-icd-loader (which provides the runtime vulkan library. This is only needed if vulkan video encoding option is used) @@ -135,10 +142,38 @@ You can record a regular video while using replay/streaming by launching GPU Scr To start/stop (and save) recording use the SIGRTMIN signal, for example `pkill -SIGRTMIN -f "^gpu-screen-recorder"`. The path to the video will be displayed in stdout when saving the video.\ This way of recording while using replay/streaming is more efficient than running GPU Screen Recorder multiple times since this way it only records the screen and encodes the video once. ## Controlling GPU Screen Recorder remotely +GPU Screen Recorder can be controlled with signals, or with commands over a unix domain socket when it's started with the `-ipc` option. +### Signals To save a video in replay mode, you need to send signal SIGUSR1 to gpu screen recorder. You can do this by running `pkill -SIGUSR1 -f "^gpu-screen-recorder"`.\ To stop recording send SIGINT to gpu screen recorder. You can do this by running `pkill -SIGINT -f "^gpu-screen-recorder"` or pressing `Ctrl-C` in the terminal that runs gpu screen recorder. When recording a regular non-replay video this will also save the video.\ To pause/unpause recording send SIGUSR2 to gpu screen recorder. You can do this by running `pkill -SIGUSR2 -f "^gpu-screen-recorder"`. This is only applicable and useful when recording (not streaming nor replay).\ There are more signals to control GPU Screen Recorder. Run `gpu-screen-recorder --help` to list them all (under `NOTES` section). +### IPC +Start GPU Screen Recorder with the `-ipc` option to also make it listen for commands on a unix domain socket, for example:\ +``` +gpu-screen-recorder -w screen -f 60 -c mp4 -r 60 -o ~/Videos -ipc "$XDG_RUNTIME_DIR/gsr.sock" +``` +and then use the `gsr-cli` program to send commands to it:\ +``` +gsr-cli -ipc "$XDG_RUNTIME_DIR/gsr.sock" save-replay 30 +gsr-cli -ipc "$XDG_RUNTIME_DIR/gsr.sock" save-replay +gsr-cli -ipc "$XDG_RUNTIME_DIR/gsr.sock" toggle-replay-recording +gsr-cli -ipc "$XDG_RUNTIME_DIR/gsr.sock" start-replay-recording +gsr-cli -ipc "$XDG_RUNTIME_DIR/gsr.sock" stop-replay-recording +gsr-cli -ipc "$XDG_RUNTIME_DIR/gsr.sock" toggle-pause +gsr-cli -ipc "$XDG_RUNTIME_DIR/gsr.sock" set-paused true +gsr-cli -ipc "$XDG_RUNTIME_DIR/gsr.sock" stop +gsr-cli -ipc "$XDG_RUNTIME_DIR/gsr.sock" status +``` +This gives the same control as the signals, with these differences: +* `save-replay` takes the number of seconds to save, instead of the fixed times that the signals provide. The whole replay buffer is saved when no number of seconds is given. +* `gsr-cli` exits with 0 only when the command succeeded and prints the reason to stderr when it didn't, so commands don't have to be sent blindly. +* `stop`, `save-replay` and `stop-replay-recording` are replied to when the file they save has been saved (and after the `-sc` script has been started), and `gsr-cli` prints the path of the saved file. +* `set-paused`, `start-replay-recording` and `stop-replay-recording` set an absolute state instead of toggling, so the result doesn't depend on the current state. +* `status` prints `running` or `not running` and exits with 0 when GPU Screen Recorder is running, which a script can use to only start replay when it isn't already running: `gsr-cli -ipc "$XDG_RUNTIME_DIR/gsr.sock" status >/dev/null || start-replay.sh`. +* The commands are sent to one specific GPU Screen Recorder instance instead of every instance that `pkill` matches. + +Run `man gsr-cli` to see all commands. The commands are newline terminated json messages, which are described in the `IPC` section of `man gpu-screen-recorder` if you want to talk to the socket directly instead of using `gsr-cli`. ## Simple way to run replay without gui Run the script `scripts/start-replay.sh` to start replay and then `scripts/save-replay.sh` to save a replay and `scripts/stop-replay.sh` to stop the replay. The videos are saved to `$HOME/Videos`. You can use these scripts to start replay at system startup if you add `scripts/start-replay.sh` to startup (this can be done differently depending on your desktop environment / window manager) and then go into diff --git a/external/sj.h b/external/sj.h new file mode 100644 index 0000000..60bea9e --- /dev/null +++ b/external/sj.h @@ -0,0 +1,156 @@ +// sj.h - v0.4 - rxi 2025 +// public domain - no warranty implied, use at your own risk + +#ifndef SJ_H +#define SJ_H + +#include <stddef.h> +#include <stdbool.h> + +typedef struct { + char *data, *cur, *end; + int depth; + char *error; +} sj_Reader; + +typedef struct { + int type; + char *start, *end; + int depth; +} sj_Value; + +enum { SJ_ERROR, SJ_END, SJ_ARRAY, SJ_OBJECT, SJ_NUMBER, SJ_STRING, SJ_BOOL, SJ_NULL }; + +sj_Reader sj_reader(char *data, size_t len); +sj_Value sj_read(sj_Reader *r); +bool sj_iter_array(sj_Reader *r, sj_Value arr, sj_Value *val); +bool sj_iter_object(sj_Reader *r, sj_Value obj, sj_Value *key, sj_Value *val); +void sj_location(sj_Reader *r, int *line, int *col); + +#endif // #ifndef SJ_H + +#ifdef SJ_IMPL + + +sj_Reader sj_reader(char *data, size_t len) { + return (sj_Reader){ .data = data, .cur = data, .end = data + len }; +} + + +static bool sj__is_number_cont(char c) { + return (c >= '0' && c <= '9') + || c == 'e' || c == 'E' || c == '.' || c == '-' || c == '+'; +} + +static bool sj__is_string(char *cur, char *end, char *expect) { + while (*expect) { + if (cur == end || *cur != *expect) { + return false; + } + expect++, cur++; + } + return true; +} + + +sj_Value sj_read(sj_Reader *r) { + sj_Value res; +top: + if (r->error) { return (sj_Value){ .type = SJ_ERROR, .start = r->cur, .end = r->cur }; } + if (r->cur == r->end) { r->error = "unexpected eof"; goto top; } + res.start = r->cur; + + switch (*r->cur) { + case ' ': case '\n': case '\r': case '\t': + case ':': case ',': + r->cur++; + goto top; + + case '-': case '0': case '1': case '2': case '3': case '4': + case '5': case '6': case '7': case '8': case '9': + res.type = SJ_NUMBER; + while (r->cur != r->end && sj__is_number_cont(*r->cur)) { r->cur++; } + break; + + case '"': + res.type = SJ_STRING; + res.start = ++r->cur; + for (;;) { + if ( r->cur == r->end) { r->error = "unclosed string"; goto top; } + if (*r->cur == '"') { break; } + if (*r->cur == '\\') { r->cur++; } + if ( r->cur != r->end) { r->cur++; } + } + res.end = r->cur++; + return res; + + case '{': case '[': + res.type = (*r->cur == '{') ? SJ_OBJECT : SJ_ARRAY; + res.depth = ++r->depth; + r->cur++; + break; + + case '}': case ']': + res.type = SJ_END; + if (--r->depth < 0) { + r->error = (*r->cur == '}') ? "stray '}'" : "stray ']'"; + goto top; + } + r->cur++; + break; + + case 'n': case 't': case 'f': + res.type = (*r->cur == 'n') ? SJ_NULL : SJ_BOOL; + if (sj__is_string(r->cur, r->end, "null")) { r->cur += 4; break; } + if (sj__is_string(r->cur, r->end, "true")) { r->cur += 4; break; } + if (sj__is_string(r->cur, r->end, "false")) { r->cur += 5; break; } + // fallthrough + + default: + r->error = "unknown token"; + goto top; + } + res.end = r->cur; + return res; +} + + +static void sj__discard_until(sj_Reader *r, int depth) { + sj_Value val; + val.type = SJ_NULL; + while (r->depth != depth && val.type != SJ_ERROR) { + val = sj_read(r); + } +} + + +bool sj_iter_array(sj_Reader *r, sj_Value arr, sj_Value *val) { + sj__discard_until(r, arr.depth); + *val = sj_read(r); + if (val->type == SJ_ERROR || val->type == SJ_END) { return false; } + return true; +} + + +bool sj_iter_object(sj_Reader *r, sj_Value obj, sj_Value *key, sj_Value *val) { + sj__discard_until(r, obj.depth); + *key = sj_read(r); + if (key->type == SJ_ERROR || key->type == SJ_END) { return false; } + *val = sj_read(r); + if (val->type == SJ_END) { r->error = "unexpected object end"; return false; } + if (val->type == SJ_ERROR) { return false; } + return true; +} + + +void sj_location(sj_Reader *r, int *line, int *col) { + int ln = 1, cl = 1; + for (char *p = r->data; p != r->cur; p++) { + if (*p == '\n') { ln++; cl = 0; } + cl++; + } + *line = ln; + *col = cl; +} + +#endif // #ifdef SJ_IMPL
\ No newline at end of file diff --git a/extra/build_ffmpeg.sh b/extra/build_ffmpeg.sh new file mode 100755 index 0000000..052a822 --- /dev/null +++ b/extra/build_ffmpeg.sh @@ -0,0 +1,199 @@ +#!/bin/sh + +# Builds ffmpeg, or one of the libraries that ffmpeg depends on, from source as a +# static library with lto. Only the components that gpu-screen-recorder uses are enabled. +# This is run by meson when the "ffmpeg_static" option is enabled. + +set -eu + +if [ $# -ne 5 ]; then + echo "usage: $0 <library> <source-dir> <build-dir> <install-prefix> <c-compiler>" >&2 + exit 1 +fi + +library=$1 +source_dir=$2 +build_dir=$3 +prefix=$4 +compiler=$5 +log_file=$build_dir/build.log +stamp_file=$prefix/$library.stamp +jobs=$(nproc 2>/dev/null || echo 4) + +export CC=$compiler +export PKG_CONFIG_PATH=$prefix/lib/pkgconfig${PKG_CONFIG_PATH:+:$PKG_CONFIG_PATH} + +require_program() { + if ! command -v "$1" >/dev/null 2>&1; then + echo "error: $1 is required to build $library from source" >&2 + exit 1 + fi +} + +require_program pkg-config +require_program make + +case $library in + nv-codec-headers) + set -- PREFIX="$prefix" + build() { + make -C "$source_dir" "$@" install + } + ;; + x264) + require_program nasm + set -- --prefix="$prefix" \ + --enable-static \ + --enable-pic \ + --enable-lto \ + --disable-cli \ + --disable-opencl + build() { + "$source_dir/configure" "$@" + make -j"$jobs" + make install + } + ;; + opus) + set -- --prefix="$prefix" \ + --disable-shared \ + --enable-static \ + --with-pic \ + --disable-doc \ + --disable-extra-programs \ + CFLAGS="-O3 -flto -fPIC" \ + LDFLAGS="-flto" + build() { + "$source_dir/configure" "$@" + make -j"$jobs" + make install + } + ;; + mbedtls) + require_program cmake + require_program python3 + set -- -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_INSTALL_PREFIX="$prefix" \ + -DCMAKE_INSTALL_LIBDIR=lib \ + -DCMAKE_INTERPROCEDURAL_OPTIMIZATION=ON \ + -DCMAKE_POSITION_INDEPENDENT_CODE=ON \ + -DENABLE_TESTING=OFF \ + -DENABLE_PROGRAMS=OFF \ + -DUSE_SHARED_MBEDTLS_LIBRARY=OFF \ + -DUSE_STATIC_MBEDTLS_LIBRARY=ON + build() { + # The whip muxer needs dtls-srtp, which mbedtls doesn't enable by default. + python3 "$source_dir/scripts/config.py" -f "$source_dir/include/mbedtls/mbedtls_config.h" set MBEDTLS_SSL_DTLS_SRTP + cmake -S "$source_dir" -B "$build_dir" "$@" + cmake --build "$build_dir" -j"$jobs" + cmake --install "$build_dir" + } + ;; + srt) + require_program cmake + set -- -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_INSTALL_PREFIX="$prefix" \ + -DCMAKE_INSTALL_LIBDIR=lib \ + -DCMAKE_INTERPROCEDURAL_OPTIMIZATION=ON \ + -DCMAKE_POSITION_INDEPENDENT_CODE=ON \ + -DENABLE_SHARED=OFF \ + -DENABLE_STATIC=ON \ + -DENABLE_APPS=OFF \ + -DENABLE_EXAMPLES=OFF \ + -DENABLE_TESTING=OFF \ + -DENABLE_UNITTESTS=OFF \ + -DUSE_ENCLIB=mbedtls \ + -DMBEDTLS_PREFIX="$prefix" \ + -DSSL_REQUIRED_MODULES=mbedtls + build() { + cmake -S "$source_dir" -B "$build_dir" "$@" + cmake --build "$build_dir" -j"$jobs" + cmake --install "$build_dir" + } + ;; + ffmpeg) + require_program nasm + + encoders=aac,flac,libx264,libopus,h264_nvenc,hevc_nvenc,av1_nvenc,h264_vaapi,hevc_vaapi,vp8_vaapi,vp9_vaapi,h264_vulkan,hevc_vulkan + muxers=mp4,mov,matroska,webm,flv,mpegts,hls,whip + protocols=file,pipe,tcp,udp,http,https,tls,rtmp,rtmps,libsrt + filters=abuffer,abuffersink,amix,aresample,aformat,anull + + if pkg-config --atleast-version=1.16 libva; then + encoders=$encoders,av1_vaapi + else + echo "warning: libva is too old for av1 vaapi encoding" >&2 + fi + + if pkg-config --atleast-version=1.4.317 vulkan; then + encoders=$encoders,av1_vulkan + else + echo "warning: the vulkan headers are too old for av1 vulkan encoding" >&2 + fi + + # ffmpeg ignores $CC and the same compiler as gpu-screen-recorder has to be + # used because lto objects are not compatible between compilers. + set -- --prefix="$prefix" \ + --cc="$compiler" \ + --pkg-config-flags=--static \ + --disable-debug \ + --disable-everything \ + --disable-autodetect \ + --disable-programs \ + --disable-doc \ + --disable-avdevice \ + --disable-swscale \ + --disable-shared \ + --enable-static \ + --enable-pic \ + --enable-lto \ + --enable-gpl \ + --enable-version3 \ + --enable-network \ + --enable-encoder="$encoders" \ + --enable-muxer="$muxers" \ + --enable-protocol="$protocols" \ + --enable-filter="$filters" \ + --enable-bsf=extract_extradata \ + --enable-libx264 \ + --enable-libopus \ + --enable-libsrt \ + --enable-mbedtls \ + --enable-ffnvcodec \ + --enable-nvenc \ + --enable-cuda \ + --enable-vaapi \ + --enable-vulkan + build() { + "$source_dir/configure" "$@" + make -j"$jobs" + make install + } + ;; + *) + echo "error: unknown library $library" >&2 + exit 1 + ;; +esac + +# Rebuilding takes a long time, only do it when the source or the configuration changed. +stamp="$source_dir $compiler $*" +if [ -f "$stamp_file" ] && [ "$(cat "$stamp_file")" = "$stamp" ]; then + exit 0 +fi + +rm -rf "$build_dir" +mkdir -p "$build_dir" + +echo "Building $library, this takes a while. Output is written to $log_file" >&2 + +( + cd "$build_dir" + build "$@" +) >"$log_file" 2>&1 || { + echo "error: failed to build $library, see $log_file for details:" >&2 + tail -n 30 "$log_file" >&2 + exit 1 +} + +printf '%s' "$stamp" > "$stamp_file" diff --git a/gpu-screen-recorder.1 b/gpu-screen-recorder.1 index 38d4df6..9f461cc 100644 --- a/gpu-screen-recorder.1 +++ b/gpu-screen-recorder.1 @@ -392,6 +392,14 @@ monotonic_microsec realtime_microsec <monotonic_microsec> <realtime_microsec> .fi (default: no). Ignored for live streaming and when output is piped. +.TP +.BI \-ipc " socket_path" +Listen for commands on a unix domain socket at +.I socket_path +instead of only accepting signals. The socket is created when GPU Screen Recorder starts and removed when it exits. +See the +.B IPC +section for the protocol. Has no effect when taking a screenshot. .SS Output Options .TP .BI \-o " output" @@ -461,110 +469,218 @@ Use .B pkill to send signals (e.g., .BR "pkill -SIGUSR1 -f ""^gpu-screen-recorder""" ). -.SH EXAMPLES -Record monitor at 60 FPS with desktop audio: +.SH IPC +When the +.B \-ipc +option is used GPU Screen Recorder also listens for commands on a unix domain socket. This gives the same control as signals, +except that a replay can be saved with an arbitrary number of seconds. The socket can only be used by the user that +started GPU Screen Recorder and it's removed when GPU Screen Recorder exits. +.PP +.BR gsr\-cli (1) +is a program that sends these commands and reports the reply, so the protocol below only has to be +implemented by programs that want to talk to the socket directly. +.PP +Requests and replies are json objects terminated by a newline. Every request is replied to. +A request has these fields: +.TP +.B id +Number that identifies the request. The reply to the request has the same id. +.TP +.B name +String with the name of the request. +.TP +.B data +Optional, the type depends on the request. .PP +A reply has these fields: +.TP +.B id +The id of the request that this is a reply to, or 0 when the request had no valid id. +.TP +.B result +Either +.B ok +or +.BR error . +.TP +.B data +Optional. For an +.B error +result this is a string that describes what went wrong. For an +.B ok +result to +.BR stop ", " save-replay " or " stop-replay-recording +this is a string with the path of the saved file. +.PP +The +.BR stop ", " save-replay " and " stop-replay-recording +requests save a file, and they are replied to when the file has been saved and the script given with the +.B \-sc +option has been started. The other requests are replied to as soon as they are accepted. +.PP +These requests are available: +.TP +.B stop +Stop and save recording (stop without save in replay mode). The reply contains the path of the +saved file, except in replay mode where nothing is saved. +.TP +.B save-replay +Save replay (replay mode only). +.B data +is an object with these optional fields: +.RS +.TP +.B seconds +The number of seconds to save, which has to be larger than 0. +.TP +.B restart-replay +true/false, which overrides the +.B \-restart\-replay\-on\-save +option for this save. Just like that option, the replay buffer is only cleared when the whole replay buffer is saved. +.RE +.IP +The whole replay buffer is saved when +.B data +is omitted or null, or when +.B seconds +is omitted or null. The reply contains the path of the saved file. +.TP +.B toggle-pause +Pause/unpause recording (not for streaming/replay). +.TP +.B set-paused +Pause/unpause recording (not for streaming/replay). +.B data +has to be true to pause or false to unpause. Unlike +.B toggle-pause +this doesn't fail when the recording is already paused/unpaused. +.TP +.B toggle-replay-recording +Start/stop regular recording during replay/streaming, which requires the +.B \-ro +option. +.TP +.B start-replay-recording +Start regular recording during replay/streaming, which requires the +.B \-ro +option. Does nothing when a recording is already running. +.TP +.B stop-replay-recording +Stop the regular recording that runs during replay/streaming. Fails when no recording is running. +The reply contains the path of the saved file. +.PP +Example: +.nf +.RS +gpu-screen-recorder -w screen -f 60 -c mp4 -r 60 -o ~/Videos -ipc "$XDG_RUNTIME_DIR/gsr.sock" & +echo '{"id":1,"name":"save-replay","data":{"seconds":30}}' | socat - "UNIX-CONNECT:$XDG_RUNTIME_DIR/gsr.sock" +.RE +.fi +which replies with the following when the replay has been saved: +.nf +.RS +{"id":1,"result":"ok","data":"/home/user/Videos/Replay_2026-08-05_14-04-22.mp4"} +.RE +.fi +.SH EXAMPLES +.B Record monitor at 60 FPS with desktop audio: .nf .RS gpu-screen-recorder -w screen -f 60 -a default_output -o video.mp4 .RE .fi .PP -Record monitor at 60 FPS with desktop audio and microphone: -.PP +.B Record monitor at 60 FPS with desktop audio and microphone: .nf .RS gpu-screen-recorder -w screen -f 60 -a "default_output|default_input" -o video.mp4 .RE .fi .PP -Record specific window (X11 only): -.PP +.B Record specific window (X11 only): .nf .RS gpu-screen-recorder -w $(xdotool selectwindow) -f 60 -o video.mp4 .RE .fi .PP -Instant replay (last 60 seconds): -.PP +.B Instant replay (last 60 seconds): .nf .RS gpu-screen-recorder -w screen -c mp4 -r 60 -o ~/Videos .RE .fi .PP -Instant replay with constant framerate (recommended for predicable ram usage): -.PP +.B Instant replay with constant framerate (recommended for predicable ram usage): .nf .RS gpu-screen-recorder -w screen -c mp4 -r 60 -bm cbr -q 40000 -o ~/Videos .RE .fi .PP -Record region using slop: -.PP +.B Record region using slop: .nf .RS gpu-screen-recorder -w $(slop) -o video.mp4 .RE .fi .PP -Record region using slurp: -.PP +.B Record region using slurp: .nf .RS gpu-screen-recorder -w $(slurp -f "%wx%h+%x+%y") -o video.mp4 .RE .fi .PP -Instant replay and launch a script when saving replay: -.PP +.B Instant replay and launch a script when saving replay: .nf .RS gpu-screen-recorder -w screen -c mp4 -r 60 -sc ./script.sh -o ~/Videos .RE .fi .PP -Stream to Twitch with constant bitrate mode: -.PP +.B Stream to Twitch with constant bitrate mode: .nf .RS gpu-screen-recorder -w screen -c flv -a default_output -bm cbr -q 8000 -o "rtmp://live.twitch.tv/app/stream_key" .RE .fi .PP -Take screenshot: -.PP +.B Take screenshot: .nf .RS gpu-screen-recorder -w screen -o screenshot.jpg .PP .RE .fi -Record screen and camera: -.PP +.B Record screen and camera: .nf .RS gpu-screen-recorder -w "screen|/dev/video0" -o video.mp4 .PP .RE .fi -Record screen and camera. The camera is located at the bottom right and flipped horizontally: -.PP +.B Record screen and camera. The camera is located at the bottom right and flipped horizontally: .nf .RS gpu-screen-recorder -w "screen|/dev/video0;halign=end;valign=end;hflip=true;width=30%;height=30%" -o video.mp4 .PP .RE .fi -Record two monitors, side by side (assuming the first monitor has a resolution of 1920x1080) -.PP +.B Record two monitors, side by side (assuming the first monitor has a resolution of 1920x1080): .nf .RS gpu-screen-recorder -w "DP-1|DP-2;x=1920" -o video.mp4 .RE .fi +.PP +.B Stream whip with an authorization key: +.nf +.RS +gpu-screen-recorder -w screen -bm cbr -q 3500 -ffmpeg-opts "authorization=streamtest;ts_buffer_size=131072" -c whip -o https://b.siobud.com/api/whip +.PP +.RE +.fi .SH FILES .TP .I ~/.config/gpu-screen-recorder/gpu-screen-recorder.env @@ -616,6 +732,9 @@ The bug may have been previously reported or may not be related to gpu-screen-r .SH COPYRIGHT Copyright © dec05eba. Licensed under GPL-3.0-only. .SH SEE ALSO +.BR gsr\-cli (1), +.BR gsr\-kms\-server (1) +.PP .UR https://git.dec05eba.com/gpu-screen-recorder Project homepage .UE diff --git a/gsr-cli.1 b/gsr-cli.1 new file mode 100644 index 0000000..14d1f51 --- /dev/null +++ b/gsr-cli.1 @@ -0,0 +1,135 @@ +.TH GSR\-CLI 1 "2026-08-02" "5.15.3" "GPU Screen Recorder CLI Manual" +.SH NAME +gsr\-cli \- Control a running GPU Screen Recorder instance +.SH SYNOPSIS +.B gsr\-cli +.B \-ipc +.I socket_path +.I command +.RI [ command_arguments... ] +.PP +.B gsr\-cli +.B \-h +| +.B \-\-help +.SH DESCRIPTION +.B gsr\-cli +sends a command to a +.BR gpu\-screen\-recorder (1) +instance that was started with the +.B \-ipc +option and reports the reply. This gives the same control as sending signals to GPU Screen Recorder, +except that a replay can be saved with an arbitrary number of seconds and that the result of the +command is known instead of being sent blindly. +.PP +.B gsr\-cli +exits with 0 when the command succeeded and 1 when it failed. When a command fails the reason +is printed to stderr. +.SH OPTIONS +.TP +.BI \-ipc " socket_path" +The unix domain socket that GPU Screen Recorder was started with. Required. +.SH COMMANDS +.TP +.B status +Check if a GPU Screen Recorder instance is listening on the socket, which is done by connecting +to the socket without sending a command. Prints +.B running +or +.B "not running" +and exits with 0 when it's running. A socket file that was left behind by a GPU Screen Recorder +instance that was killed counts as not running. +.TP +.B stop +Stop and save recording (stop without save in replay mode). Waits until the recording has been +saved and prints the path of the saved file. Nothing is printed in replay mode since nothing is +saved. +.TP +.B toggle-pause +Pause/unpause recording (not for streaming/replay). +.TP +.BR set-paused " true|false" +Pause/unpause recording (not for streaming/replay). Unlike +.B toggle-pause +this doesn't fail when the recording is already paused/unpaused, so the result doesn't depend on +the current state. +.TP +.B toggle-replay-recording +Start/stop regular recording during replay/streaming, which requires GPU Screen Recorder to run +with the +.B \-ro +option. +.TP +.B start-replay-recording +Start regular recording during replay/streaming, which requires GPU Screen Recorder to run +with the +.B \-ro +option. Does nothing when a recording is already running. +.TP +.B stop-replay-recording +Stop the regular recording that runs during replay/streaming. Waits until the recording has been +saved and prints the path of the saved file. Fails when no recording is running. +.TP +.BR save-replay " [" \fIseconds\fR "] [" restart-replay=true|false ] +Save replay (replay mode only). The number of seconds has to be larger than 0. The whole replay +buffer is saved when no number of seconds is given. Waits until the replay has been saved and +prints the path of the saved file. +.B restart-replay +overrides the +.B \-restart\-replay\-on\-save +option of GPU Screen Recorder for this save, which clears the replay buffer after the whole replay +buffer has been saved. +.SH EXAMPLES +Start GPU Screen Recorder in replay mode with an ipc socket: +.RS +.nf +gpu-screen-recorder -w screen -f 60 -c mp4 -r 60 -o ~/Videos -ipc "$XDG_RUNTIME_DIR/gsr.sock" & +.fi +.RE +.PP +Save the last 30 seconds: +.RS +.nf +gsr-cli -ipc "$XDG_RUNTIME_DIR/gsr.sock" save-replay 30 +.fi +.RE +.PP +Save the whole replay buffer and clear it: +.RS +.nf +gsr-cli -ipc "$XDG_RUNTIME_DIR/gsr.sock" save-replay restart-replay=true +.fi +.RE +.PP +Start replay only when it isn't already running: +.RS +.nf +gsr-cli -ipc "$XDG_RUNTIME_DIR/gsr.sock" status >/dev/null || start-replay.sh +.fi +.RE +.SH NOTES +.IP \(bu 3 +The socket can only be used by the user that started GPU Screen Recorder. +.IP \(bu 3 +The +.BR stop ", " save-replay " and " stop-replay-recording +commands are replied to when the file has been saved and the script given with the +.B \-sc +option has been started, and the reply contains the path of the saved file. The other commands are +replied to as soon as GPU Screen Recorder accepts them. +.IP \(bu 3 +GPU Screen Recorder creates the socket before it has finished starting up, so a command that is +sent while it's starting up is replied to when the recording starts. +.SH AUTHORS +gsr\-cli was written by the GPU Screen Recorder contributors. +.SH REPORTING BUGS +Report bugs to: +.UR mailto:dec05eba@protonmail.com +dec05eba@protonmail.com +.UE . +.SH COPYRIGHT +Copyright © dec05eba. Licensed under GPL-3.0-only. +.SH SEE ALSO +.BR gpu\-screen\-recorder (1) +.PP +Project homepage: <https://git.dec05eba.com/gpu-screen-recorder/about/> diff --git a/include/args_parser.h b/include/args_parser.h index e8c68d8..2954dde 100644 --- a/include/args_parser.h +++ b/include/args_parser.h @@ -5,10 +5,11 @@ #include <stdint.h> #include "defs.h" #include "vec2.h" +#include "recorder/settings.h" typedef struct gsr_egl gsr_egl; -#define NUM_ARGS 38 +#define NUM_ARGS 39 typedef enum { GSR_CAPTURE_SOURCE_TYPE_WINDOW, @@ -56,66 +57,31 @@ typedef struct { } typed_value; } Arg; +/* These return the exit code that gpu-screen-recorder should exit with */ typedef struct { - void (*version)(void *userdata); - void (*info)(void *userdata); - void (*list_audio_devices)(void *userdata); - void (*list_application_audio)(void *userdata); - void (*list_v4l2_devices)(void *userdata); - void (*list_capture_options)(const char *card_path, void *userdata); - void (*list_monitors)(void *userdata); + int (*version)(void *userdata); + int (*info)(void *userdata); + int (*list_audio_devices)(void *userdata); + int (*list_application_audio)(void *userdata); + int (*list_v4l2_devices)(void *userdata); + int (*list_capture_options)(const char *card_path, void *userdata); + int (*list_monitors)(void *userdata); } args_handlers; +typedef enum { + ARGS_PARSE_RESULT_ERROR, + ARGS_PARSE_RESULT_OK, + /* One of the |args_handlers| ran and |command_exit_code| is set */ + ARGS_PARSE_RESULT_COMMAND_HANDLED +} args_parse_result; + typedef struct { Arg args[NUM_ARGS]; - - gsr_video_encoder_hardware video_encoder; - gsr_pixel_format pixel_format; - gsr_framerate_mode framerate_mode; - gsr_color_range color_range; - gsr_tune tune; - gsr_video_codec video_codec; - gsr_audio_codec audio_codec; - gsr_bitrate_mode bitrate_mode; - gsr_video_quality video_quality; - gsr_replay_storage replay_storage; - - const char *capture_source; - const char *container_format; - const char *filename; - const char *replay_recording_directory; - const char *portal_session_token_filepath; - const char *recording_saved_script; - const char *ffmpeg_opts; - const char *ffmpeg_video_opts; - const char *ffmpeg_audio_opts; - bool verbose; - bool gl_debug; - bool fallback_cpu_encoding; - bool low_power; - bool exclude_metadata; - bool record_cursor; - bool date_folders; - bool restore_portal_session; - bool restart_replay_on_save; - bool write_first_frame_ts; - bool is_replaying; - bool is_livestream; - bool is_output_piped; - bool low_latency_recording; - bool very_old_gpu; - int64_t video_bitrate; - int64_t audio_bitrate; - int64_t fps; - int64_t replay_buffer_size_secs; - double keyint; - vec2i output_resolution; - vec2i region_size; - vec2i region_position; + gsr_recorder_settings settings; } args_parser; /* |argv| is stored as a reference */ -bool args_parser_parse(args_parser *self, int argc, char **argv, const args_handlers *args_handlers, void *userdata); +args_parse_result args_parser_parse(args_parser *self, int argc, char **argv, const args_handlers *args_handlers, void *userdata, int *command_exit_code); void args_parser_deinit(args_parser *self); bool args_parser_validate_with_gl_info(args_parser *self, gsr_egl *egl); diff --git a/include/cli/commands.h b/include/cli/commands.h new file mode 100644 index 0000000..9ab00f0 --- /dev/null +++ b/include/cli/commands.h @@ -0,0 +1,15 @@ +#ifndef GSR_CLI_COMMANDS_H +#define GSR_CLI_COMMANDS_H + +/* These are the |args_handlers| of the argument parser. They return the exit code that gpu-screen-recorder should exit with */ +int version_command(void *userdata); +int info_command(void *userdata); +int list_audio_devices_command(void *userdata); +int list_application_audio_command(void *userdata); +int list_v4l2_devices(void *userdata); +int list_capture_options_command(const char *card_path, void *userdata); +int list_monitors_command(void *userdata); + +void run_recording_saved_script_async(const char *script_file, const char *video_file, const char *type); + +#endif /* GSR_CLI_COMMANDS_H */ diff --git a/include/cli/ipc.h b/include/cli/ipc.h new file mode 100644 index 0000000..0c4c076 --- /dev/null +++ b/include/cli/ipc.h @@ -0,0 +1,105 @@ +#ifndef GSR_CLI_IPC_H +#define GSR_CLI_IPC_H + +#include <stdbool.h> +#include <stddef.h> +#include <stdint.h> +#include <limits.h> +#include <pthread.h> + +#define GSR_IPC_MAX_CLIENTS 8 +#define GSR_IPC_MAX_REQUEST_SIZE 4096 +#define GSR_IPC_MAX_ERROR_MESSAGE_SIZE 256 +#define GSR_IPC_MAX_ESCAPED_ERROR_MESSAGE_SIZE (GSR_IPC_MAX_ERROR_MESSAGE_SIZE*6) +#define GSR_IPC_MAX_ESCAPED_DATA_SIZE PATH_MAX +#define GSR_IPC_MAX_REPLY_SIZE (GSR_IPC_MAX_ESCAPED_ERROR_MESSAGE_SIZE + GSR_IPC_MAX_ESCAPED_DATA_SIZE + 128) +#define GSR_IPC_CLIENT_SEND_BUFFER_SIZE (GSR_IPC_MAX_REPLY_SIZE*2) + +/* + These are called from the ipc thread while the recording is running. + Return false and write a message to |error_message| to reply to the request with an error. + The reply to the requests that match a |gsr_ipc_deferred_request_type| is not sent when the handler + succeeds. It's sent when the matching gsr_ipc_complete_request is called. +*/ +typedef struct { + bool (*stop)(char *error_message, size_t error_message_size, void *userdata); + bool (*toggle_pause)(char *error_message, size_t error_message_size, void *userdata); + bool (*set_paused)(bool paused, char *error_message, size_t error_message_size, void *userdata); + bool (*toggle_replay_recording)(char *error_message, size_t error_message_size, void *userdata); + bool (*start_replay_recording)(char *error_message, size_t error_message_size, void *userdata); + bool (*stop_replay_recording)(char *error_message, size_t error_message_size, void *userdata); + /* + |seconds| is GSR_SAVE_REPLAY_SECONDS_FULL when the whole replay buffer should be saved. + |restart_replay| overrides the -restart-replay-on-save option for this save when |has_restart_replay| is set. + */ + bool (*save_replay)(int seconds, bool has_restart_replay, bool restart_replay, char *error_message, size_t error_message_size, void *userdata); + void *userdata; +} gsr_ipc_handlers; + +/* Requests that are replied to when the file they save has been saved (see gsr_ipc_complete_request) */ +typedef enum { + GSR_IPC_DEFERRED_REQUEST_STOP, + GSR_IPC_DEFERRED_REQUEST_SAVE_REPLAY, + GSR_IPC_DEFERRED_REQUEST_STOP_REPLAY_RECORDING, + GSR_IPC_DEFERRED_REQUEST_TYPE_COUNT +} gsr_ipc_deferred_request_type; + +typedef enum { + GSR_IPC_DEFERRED_REQUEST_STATE_EMPTY, + GSR_IPC_DEFERRED_REQUEST_STATE_PENDING, + GSR_IPC_DEFERRED_REQUEST_STATE_COMPLETED +} gsr_ipc_deferred_request_state; + +typedef struct { + gsr_ipc_deferred_request_state state; + int client_fd; + int64_t request_id; + bool success; + bool has_filepath; + char filepath[PATH_MAX]; +} gsr_ipc_deferred_request; + +typedef struct { + int fd; + char request[GSR_IPC_MAX_REQUEST_SIZE]; + size_t request_size; + bool request_too_large; + char send_buffer[GSR_IPC_CLIENT_SEND_BUFFER_SIZE]; + size_t send_buffer_size; +} gsr_ipc_client; + +/* Receives newline terminated json requests on a unix domain socket and replies to every request */ +typedef struct { + bool initialized; + int socket_fd; + int poll_fd; + bool socket_bound; + char socket_filepath[PATH_MAX]; + int wakeup_pipe[2]; + gsr_ipc_client clients[GSR_IPC_MAX_CLIENTS]; + int num_clients; + gsr_ipc_handlers handlers; + gsr_ipc_deferred_request deferred_requests[GSR_IPC_DEFERRED_REQUEST_TYPE_COUNT]; + pthread_mutex_t deferred_requests_mutex; + bool deferred_requests_mutex_created; + pthread_t thread; + bool thread_running; +} gsr_ipc; + +/* Returns a |gsr_error| value. Creates the socket, requests are not handled until gsr_ipc_start is called */ +int gsr_ipc_init(gsr_ipc *self, const char *socket_filepath); +void gsr_ipc_deinit(gsr_ipc *self); + +/* Returns a |gsr_error| value. Starts handling requests. Does nothing if |self| hasn't been initialized */ +int gsr_ipc_start(gsr_ipc *self, const gsr_ipc_handlers *handlers); +/* Stops handling requests. Does nothing if the ipc hasn't been started */ +void gsr_ipc_stop(gsr_ipc *self); + +/* + Sends the reply to the pending request of |type|, or does nothing when there is no pending request of that type. + |filepath| is the path of the saved file and it can be NULL when nothing was saved. + This is safe to call from any thread. Does nothing if |self| hasn't been initialized. +*/ +void gsr_ipc_complete_request(gsr_ipc *self, gsr_ipc_deferred_request_type type, bool success, const char *filepath); + +#endif /* GSR_CLI_IPC_H */ diff --git a/include/defs.h b/include/defs.h index 4bb10d8..5ffb546 100644 --- a/include/defs.h +++ b/include/defs.h @@ -11,6 +11,7 @@ typedef enum { GSR_GPU_VENDOR_INTEL, GSR_GPU_VENDOR_NVIDIA, GSR_GPU_VENDOR_BROADCOM, + GSR_GPU_VENDOR_APPLE } gsr_gpu_vendor; typedef struct { diff --git a/include/encoder/encoder.h b/include/encoder/encoder.h index b46deb5..15009f1 100644 --- a/include/encoder/encoder.h +++ b/include/encoder/encoder.h @@ -41,7 +41,11 @@ typedef struct { } gsr_encoder; bool gsr_encoder_init(gsr_encoder *self, gsr_replay_storage replay_storage, size_t replay_buffer_num_packets, double replay_buffer_time, const char *replay_directory); -void gsr_encoder_deinit(gsr_encoder *self); +/* + When |exiting| is set the memory of the replay buffer is left to the operating system to free, which makes + this much faster when the replay buffer holds several gigabytes of data. See gsr_replay_buffer_destroy_at_exit. +*/ +void gsr_encoder_deinit(gsr_encoder *self, bool exiting); void gsr_encoder_receive_packets(gsr_encoder *self, AVCodecContext *codec_context, int64_t pts, int stream_index); /* Returns the id to the recording destination, or -1 on error */ diff --git a/include/ffmpeg_utils.h b/include/ffmpeg_utils.h new file mode 100644 index 0000000..6d7481b --- /dev/null +++ b/include/ffmpeg_utils.h @@ -0,0 +1,19 @@ +#ifndef GSR_FFMPEG_UTILS_H +#define GSR_FFMPEG_UTILS_H + +#include <stdbool.h> + +typedef struct AVFormatContext AVFormatContext; + +const char* gsr_av_error_to_string(int err); + +/* Marks that a packet has been successfully written to |av_format_context|, see gsr_av_format_context_write_trailer */ +void gsr_av_format_context_mark_packet_written(AVFormatContext *av_format_context); +/* + The same as av_write_trailer, except that the trailer is not written when no packet has been written to a muxer + that uses the hybrid_fragmented movflags option (see set_format_context_options), because the mov muxer in FFmpeg + crashes when it finalizes a hybrid_fragmented file that has no packets. Returns 0 on success, just like av_write_trailer. +*/ +int gsr_av_format_context_write_trailer(AVFormatContext *av_format_context); + +#endif /* GSR_FFMPEG_UTILS_H */ diff --git a/include/json.h b/include/json.h new file mode 100644 index 0000000..3bf8a9e --- /dev/null +++ b/include/json.h @@ -0,0 +1,15 @@ +#ifndef GSR_JSON_H +#define GSR_JSON_H + +#include <stdbool.h> +#include <stddef.h> +#include <stdint.h> +#include "../external/sj.h" + +bool gsr_json_string_equals(const sj_Value *value, const char *str); +/* Fails if |value| is not a json number without a fractional part */ +bool gsr_json_number_to_int64(const sj_Value *value, int64_t *result); +/* An escaped string can become 6 times as large as |str|. The result is truncated when it doesn't fit in |buffer| */ +void gsr_json_escape_string(char *buffer, size_t buffer_size, const char *str); + +#endif /* GSR_JSON_H */ diff --git a/include/log.h b/include/log.h new file mode 100644 index 0000000..e92ac99 --- /dev/null +++ b/include/log.h @@ -0,0 +1,20 @@ +#ifndef GSR_LOG_H +#define GSR_LOG_H + +typedef enum { + GSR_LOG_LEVEL_DEBUG, + GSR_LOG_LEVEL_INFO, + GSR_LOG_LEVEL_WARNING, + GSR_LOG_LEVEL_ERROR +} gsr_log_level; + +/* The handler may be called from any thread. |message| has no prefix, level name or trailing newline */ +typedef void (*gsr_log_handler)(gsr_log_level level, const char *message, void *userdata); + +void gsr_log(gsr_log_level level, const char *fmt, ...) __attribute__((format(printf, 2, 3))); +/* Messages below |level| are discarded */ +void gsr_log_set_level(gsr_log_level level); +/* A NULL |handler| restores the default handler which prints "gsr <level>: <message>" to stderr */ +void gsr_log_set_handler(gsr_log_handler handler, void *userdata); + +#endif /* GSR_LOG_H */ diff --git a/include/recorder/audio_capture.h b/include/recorder/audio_capture.h new file mode 100644 index 0000000..4f757c9 --- /dev/null +++ b/include/recorder/audio_capture.h @@ -0,0 +1,92 @@ +#ifndef GSR_RECORDER_AUDIO_CAPTURE_H +#define GSR_RECORDER_AUDIO_CAPTURE_H + +#include <stdbool.h> +#include <stddef.h> +#include <stdatomic.h> +#include <pthread.h> +#include "../sound.h" +#include "recording_clock.h" +#include "../encoder/encoder.h" +#include "audio_input.h" + +#ifdef GSR_APP_AUDIO +#include "../pipewire_audio.h" +#endif + +#include <libavcodec/avcodec.h> +#include <libavfilter/avfilter.h> + +#define GSR_MAX_AUDIO_SOURCES_PER_TRACK 32 + +typedef struct gsr_audio_capture gsr_audio_capture; +typedef struct gsr_audio_track gsr_audio_track; +typedef struct gsr_audio_device_capture gsr_audio_device_capture; + +typedef struct { + gsr_audio_capture *audio_capture; + gsr_audio_track *track; + gsr_audio_device_capture *device; +} gsr_audio_device_thread_userdata; + +struct gsr_audio_device_capture { + SoundDevice sound_device; + gsr_audio_input audio_input; + AVFilterContext *src_filter_ctx; + AVFrame *frame; + pthread_t thread; + bool thread_created; + gsr_audio_device_thread_userdata thread_userdata; +}; + +/* TODO: Instead of having a thread for each audio device, have one thread for all of them and read the data with non-blocking read */ +struct gsr_audio_track { + char name[GSR_AUDIO_TRACK_NAME_MAX_SIZE]; + AVCodecContext *codec_context; + gsr_audio_device_capture *audio_devices; + size_t num_audio_devices; + AVFilterGraph *graph; + AVFilterContext *sink; + int stream_index; + int64_t pts; +}; + +struct gsr_audio_capture { + gsr_audio_track *tracks; + size_t num_tracks; + size_t capacity_tracks; + + pthread_mutex_t filter_mutex; + bool filter_mutex_initialized; + pthread_t amix_thread; + bool amix_thread_created; + uint8_t *empty_audio; + + gsr_encoder *encoder; + gsr_recording_clock *clock; + const atomic_int *running; +}; + +/* Returns a |gsr_error| value */ +int gsr_audio_capture_init(gsr_audio_capture *self, gsr_encoder *encoder, gsr_recording_clock *clock, const atomic_int *running); +void gsr_audio_capture_deinit(gsr_audio_capture *self); + +bool gsr_audio_capture_add_track(gsr_audio_capture *self, const gsr_audio_track *track); +/* Allocates the silence buffer and starts one thread per audio device, plus the amix thread when |uses_amix| is set */ +int gsr_audio_capture_start(gsr_audio_capture *self, int audio_max_frame_size, bool uses_amix); +void gsr_audio_capture_join_threads(gsr_audio_capture *self); +void gsr_audio_capture_lock_filter(gsr_audio_capture *self); +void gsr_audio_capture_unlock_filter(gsr_audio_capture *self); + +/* Returns 0 on success, or a negative value on failure. |src_filter_ctx| must have room for |num_sources| items */ +int gsr_audio_init_filter_graph(AVCodecContext *audio_codec_context, AVFilterGraph **graph, AVFilterContext **sink, AVFilterContext **src_filter_ctx, size_t num_sources); + +/* Returns a |gsr_error| value. Opens the sound devices of one audio track */ +int gsr_audio_track_init_device_inputs(gsr_audio_track *self, const gsr_merged_audio_inputs *merged_audio_inputs, AVCodecContext *audio_codec_context, int num_channels, double num_audio_frames_shift, AVFilterContext **src_filter_ctx, bool use_amix); +#ifdef GSR_APP_AUDIO +/* Returns a |gsr_error| value. Creates a combined sink that application audio is routed to */ +int gsr_audio_track_init_application_input(gsr_audio_track *self, const gsr_merged_audio_inputs *merged_audio_inputs, AVCodecContext *audio_codec_context, int num_channels, double num_audio_frames_shift, gsr_pipewire_audio *pipewire_audio); +#endif +void gsr_audio_track_deinit(gsr_audio_track *self); + +#endif /* GSR_RECORDER_AUDIO_CAPTURE_H */ diff --git a/include/recorder/audio_codec.h b/include/recorder/audio_codec.h new file mode 100644 index 0000000..d432abd --- /dev/null +++ b/include/recorder/audio_codec.h @@ -0,0 +1,23 @@ +#ifndef GSR_RECORDER_AUDIO_CODEC_H +#define GSR_RECORDER_AUDIO_CODEC_H + +#include <stdbool.h> +#include <stdint.h> +#include "../defs.h" +#include "../sound.h" + +#include <libavcodec/avcodec.h> + +#define GSR_AUDIO_SAMPLE_RATE 48000 + +gsr_audio_format audio_codec_context_get_audio_format(const AVCodecContext *audio_codec_context); +enum AVSampleFormat audio_format_to_sample_format(const gsr_audio_format audio_format); + +AVCodecContext* create_audio_codec_context(int fps, gsr_audio_codec audio_codec, bool mix_audio, int64_t audio_bitrate); +bool open_audio(AVCodecContext *audio_codec_context, const char *ffmpeg_audio_opts); +AVFrame* create_audio_frame(AVCodecContext *audio_codec_context); + +double audio_codec_get_desired_delay(gsr_audio_codec audio_codec, int fps); +int audio_codec_get_frame_size(gsr_audio_codec audio_codec); + +#endif /* GSR_RECORDER_AUDIO_CODEC_H */ diff --git a/include/recorder/audio_input.h b/include/recorder/audio_input.h new file mode 100644 index 0000000..f6cb87e --- /dev/null +++ b/include/recorder/audio_input.h @@ -0,0 +1,66 @@ +#ifndef GSR_RECORDER_AUDIO_INPUT_H +#define GSR_RECORDER_AUDIO_INPUT_H + +#include <stdbool.h> +#include <stddef.h> +#include "../sound.h" + +#define GSR_AUDIO_INPUT_NAME_MAX_SIZE 256 +#define GSR_AUDIO_TRACK_NAME_MAX_SIZE 512 + +typedef enum { + GSR_AUDIO_INPUT_TYPE_DEVICE, + GSR_AUDIO_INPUT_TYPE_APPLICATION +} gsr_audio_input_type; + +typedef struct { + char name[GSR_AUDIO_INPUT_NAME_MAX_SIZE]; + gsr_audio_input_type type; + bool inverted; +} gsr_audio_input; + +typedef struct { + char track_name[GSR_AUDIO_TRACK_NAME_MAX_SIZE]; + bool has_custom_name; + gsr_audio_input *items; + size_t num_items; + size_t capacity_items; +} gsr_merged_audio_inputs; + +typedef struct { + gsr_merged_audio_inputs *items; + size_t num_items; + size_t capacity_items; +} gsr_audio_input_tracks; + +typedef struct { + char name[GSR_AUDIO_INPUT_NAME_MAX_SIZE]; +} gsr_app_audio_name; + +typedef struct { + gsr_app_audio_name *items; + size_t num_items; + size_t capacity_items; +} gsr_app_audio_names; + +bool gsr_app_audio_names_add(gsr_app_audio_names *self, const char *name); +void gsr_app_audio_names_deinit(gsr_app_audio_names *self); + +/* Returns a |gsr_error| value. Parses one -a option value, which may contain multiple audio inputs separated by | */ +int gsr_merged_audio_inputs_parse(gsr_merged_audio_inputs *self, const char *str); +bool gsr_merged_audio_inputs_add(gsr_merged_audio_inputs *self, const gsr_audio_input *audio_input); +void gsr_merged_audio_inputs_deinit(gsr_merged_audio_inputs *self); + +/* Returns a |gsr_error| value. Parses every -a option value and validates that the audio devices exist */ +int gsr_audio_input_tracks_parse(gsr_audio_input_tracks *self, const char **audio_input_args, int num_audio_input_args, const gsr_audio_devices *audio_devices); +bool gsr_audio_input_tracks_add(gsr_audio_input_tracks *self, const gsr_merged_audio_inputs *merged_audio_inputs); +void gsr_audio_input_tracks_deinit(gsr_audio_input_tracks *self); + +bool gsr_audio_inputs_has_app_audio(const gsr_merged_audio_inputs *self); +bool gsr_audio_inputs_should_use_amix(const gsr_merged_audio_inputs *self); +bool gsr_audio_input_tracks_has_app_audio(const gsr_audio_input_tracks *self); +bool gsr_audio_input_tracks_should_use_amix(const gsr_audio_input_tracks *self); +/* Returns a |gsr_error| value. Warns about application audio names that don't match any running application */ +int gsr_audio_input_tracks_validate_app_audio(const gsr_audio_input_tracks *self, const gsr_app_audio_names *app_audio_names); + +#endif /* GSR_RECORDER_AUDIO_INPUT_H */ diff --git a/include/recorder/capture_setup.h b/include/recorder/capture_setup.h new file mode 100644 index 0000000..fd4b7e7 --- /dev/null +++ b/include/recorder/capture_setup.h @@ -0,0 +1,49 @@ +#ifndef GSR_RECORDER_CAPTURE_SETUP_H +#define GSR_RECORDER_CAPTURE_SETUP_H + +#include <stdbool.h> +#include <stddef.h> +#include "../egl.h" +#include "../cursor.h" +#include "../kde_night_light.h" +#include "../capture/capture.h" +#include "../../kms/client/kms_client.h" +#include "capture_source.h" +#include "settings.h" + +#include <X11/Xlib.h> + +typedef struct { + gsr_kms_client kms_client; + bool kms_client_initialized; + gsr_kms_response kms_response; + gsr_kde_night_light *kde_night_light; + bool kde_night_light_initialized; + gsr_cursor x11_cursor; + Display *x11_cursor_display; +} gsr_capture_deps; + +typedef struct { + gsr_capture *capture; + gsr_capture_metadata metadata; + gsr_capture_source *capture_source; +} gsr_video_source; + +typedef struct { + gsr_video_source *items; + size_t num_items; +} gsr_video_sources; + +void gsr_capture_deps_init(gsr_capture_deps *self); +void gsr_capture_deps_init_cursor(gsr_capture_deps *self, gsr_egl *egl, bool record_cursor); +void gsr_capture_deps_deinit(gsr_capture_deps *self); +void gsr_capture_deps_cleanup_kms_fds(gsr_capture_deps *self); +void gsr_capture_deps_update_kms(gsr_capture_deps *self); + +/* Returns a |gsr_error| value, or the error returned by gsr_capture_start. |video_size| is set to the size of all capture sources combined */ +int gsr_video_sources_create(gsr_video_sources *self, const gsr_recorder_settings *settings, gsr_egl *egl, gsr_capture_deps *deps, bool prefer_ximage, gsr_capture_sources *capture_sources, vec2i *video_size); +void gsr_video_sources_update_with_real_video_size(gsr_video_sources *self, vec2i video_size); +bool gsr_video_sources_uses_external_image(const gsr_video_sources *self); +void gsr_video_sources_deinit(gsr_video_sources *self); + +#endif /* GSR_RECORDER_CAPTURE_SETUP_H */ diff --git a/include/recorder/capture_source.h b/include/recorder/capture_source.h new file mode 100644 index 0000000..503c620 --- /dev/null +++ b/include/recorder/capture_source.h @@ -0,0 +1,60 @@ +#ifndef GSR_RECORDER_CAPTURE_SOURCE_H +#define GSR_RECORDER_CAPTURE_SOURCE_H + +#include <stdbool.h> +#include <stdint.h> +#include <stddef.h> +#include "../vec2.h" +#include "../args_parser.h" +#include "../capture/capture.h" +#include "../capture/v4l2.h" + +#define GSR_CAPTURE_SOURCE_NAME_MAX_SIZE 256 + +typedef enum { + VVEC2I_TYPE_PIXELS, + VVEC2I_TYPE_SCALAR +} vvec2i_type; + +typedef struct { + int x; + int y; + vvec2i_type x_type; + vvec2i_type y_type; +} vvec2i; + +typedef struct { + char name[GSR_CAPTURE_SOURCE_NAME_MAX_SIZE]; + CaptureSourceType type; + gsr_capture_alignment halign; + gsr_capture_alignment valign; + gsr_capture_v4l2_pixfmt v4l2_pixfmt; + uint32_t flip; + vvec2i pos; + vvec2i size; + vec2i region_pos; + vec2i region_size; + bool region_set; + int64_t window_id; + int camera_fps; + vec2i camera_resolution; +} gsr_capture_source; + +typedef struct { + gsr_capture_source *items; + size_t num_items; + size_t capacity_items; +} gsr_capture_sources; + +void gsr_capture_source_init(gsr_capture_source *self, vec2i region_position, vec2i region_size); + +/* Returns a |gsr_error| value. Parses the -w option value, which may contain multiple capture sources separated by | */ +int gsr_capture_sources_parse(gsr_capture_sources *self, const char *capture_source_arg, vec2i region_position, vec2i region_size); +void gsr_capture_sources_deinit(gsr_capture_sources *self); + +bool gsr_capture_sources_has_type(const gsr_capture_sources *self, CaptureSourceType type); +bool gsr_capture_sources_has_damage_tracked_target(const gsr_capture_sources *self); +bool gsr_capture_sources_has_region_set(const gsr_capture_sources *self); +bool gsr_capture_sources_has_monitor_or_region(const gsr_capture_sources *self); + +#endif /* GSR_RECORDER_CAPTURE_SOURCE_H */ diff --git a/include/recorder/codec_select.h b/include/recorder/codec_select.h new file mode 100644 index 0000000..ff4b064 --- /dev/null +++ b/include/recorder/codec_select.h @@ -0,0 +1,30 @@ +#ifndef GSR_RECORDER_CODEC_SELECT_H +#define GSR_RECORDER_CODEC_SELECT_H + +#include <stdbool.h> +#include "../defs.h" +#include "../egl.h" +#include "../vec2.h" +#include "../codec_query/codec_query.h" +#include "settings.h" + +#include <libavcodec/avcodec.h> + +typedef struct gsr_video_encoder gsr_video_encoder; + +gsr_video_encoder* create_video_encoder(gsr_egl *egl, const gsr_recorder_settings *settings); + +bool get_supported_video_codecs(gsr_egl *egl, gsr_video_codec video_codec, bool use_software_video_encoder, bool cleanup, gsr_supported_video_codecs *video_codecs); +void set_supported_video_codecs_ffmpeg(gsr_supported_video_codecs *supported_video_codecs, gsr_supported_video_codecs *supported_video_codecs_vulkan, gsr_gpu_vendor vendor); + +vec2i codec_get_max_resolution(gsr_video_codec video_codec, bool use_software_video_encoder, const gsr_supported_video_codecs *supported_video_codecs); +bool codec_supports_resolution(vec2i codec_max_resolution, vec2i capture_resolution); +/* Returns -1 if none is available */ +gsr_video_codec select_appropriate_video_codec_automatically(vec2i video_size, const gsr_supported_video_codecs *supported_video_codecs); +void force_cpu_encoding(gsr_recorder_settings *settings); + +gsr_audio_codec select_audio_codec_with_fallback(gsr_audio_codec audio_codec, const char *file_extension, bool uses_amix); +/* Returns a |gsr_error| value. |settings| video codec and encoder are updated to the codec that was selected */ +int select_video_codec_with_fallback(vec2i video_size, gsr_recorder_settings *settings, const char *file_extension, gsr_egl *egl, bool *low_power, const AVCodec **video_codec); + +#endif /* GSR_RECORDER_CODEC_SELECT_H */ diff --git a/include/recorder/error.h b/include/recorder/error.h new file mode 100644 index 0000000..ba2ae1d --- /dev/null +++ b/include/recorder/error.h @@ -0,0 +1,23 @@ +#ifndef GSR_RECORDER_ERROR_H +#define GSR_RECORDER_ERROR_H + +/* The negated value of each error is the exit code that gpu-screen-recorder exits with */ +typedef enum { + GSR_ERROR_OK = 0, + GSR_ERROR_GENERIC = -1, + GSR_ERROR_UNSUPPORTED = -2, + GSR_ERROR_CAPTURE_FAILED = -3, + GSR_ERROR_VIDEO_CODEC_QUERY_FAILED = -11, + GSR_ERROR_OPENGL_LOAD_FAILED = -22, + GSR_ERROR_AUDIO_DEVICE_NOT_FOUND = -50, + GSR_ERROR_MONITOR_NOT_FOUND = -51, + GSR_ERROR_NO_VIDEO_CODEC_AVAILABLE = -52, + GSR_ERROR_VIDEO_CODEC_RESOLUTION_UNSUPPORTED = -53, + GSR_ERROR_VIDEO_CODEC_UNSUPPORTED = -54 +} gsr_error; + +static inline int gsr_error_to_exit_code(int error) { + return error < 0 ? -error : 0; +} + +#endif /* GSR_RECORDER_ERROR_H */ diff --git a/include/recorder/muxer.h b/include/recorder/muxer.h new file mode 100644 index 0000000..e47cdde --- /dev/null +++ b/include/recorder/muxer.h @@ -0,0 +1,40 @@ +#ifndef GSR_RECORDER_MUXER_H +#define GSR_RECORDER_MUXER_H + +#include <stdbool.h> +#include <stddef.h> +#include <limits.h> +#include "audio_capture.h" +#include "capture_setup.h" +#include "settings.h" + +#include <libavformat/avformat.h> + +typedef struct { + const gsr_audio_track *audio_track; + AVStream *stream; +} gsr_recording_audio_stream; + +typedef struct { + AVFormatContext *av_format_context; + AVStream *video_stream; + gsr_recording_audio_stream *audio_streams; + size_t num_audio_streams; +} gsr_recording_output; + +AVStream* create_stream(AVFormatContext *av_format_context, AVCodecContext *codec_context); +bool add_hdr_metadata_to_video_stream(gsr_capture *cap, AVStream *video_stream); +void set_format_context_options(AVFormatContext *av_format_context); +void av_write_header(AVFormatContext *av_format_context, const char *ffmpeg_opts); + +/* Returns false on failure. Creates the output file and its video and audio streams */ +bool gsr_recording_output_start(gsr_recording_output *self, const char *filename, const gsr_recorder_settings *settings, AVCodecContext *video_codec_context, const gsr_audio_capture *audio_capture, bool hdr, gsr_video_sources *video_sources); +bool gsr_recording_output_stop(gsr_recording_output *self); +gsr_recording_audio_stream* gsr_recording_output_get_audio_stream_by_index(gsr_recording_output *self, int stream_index); + +/* |filepath| should be at least PATH_MAX bytes in size. Creates the directories of the filepath */ +bool gsr_create_new_recording_filepath_from_timestamp(char *filepath, size_t filepath_size, const char *directory, const char *filename_prefix, const char *file_extension, bool date_folders); + +size_t calculate_estimated_replay_buffer_packets(int64_t replay_buffer_size_secs, int fps, gsr_audio_codec audio_codec, const gsr_audio_input_tracks *audio_inputs); + +#endif /* GSR_RECORDER_MUXER_H */ diff --git a/include/recorder/recorder.h b/include/recorder/recorder.h new file mode 100644 index 0000000..9ff13bd --- /dev/null +++ b/include/recorder/recorder.h @@ -0,0 +1,69 @@ +#ifndef GSR_RECORDER_RECORDER_H +#define GSR_RECORDER_RECORDER_H + +#include <stdbool.h> +#include "settings.h" +#include "capture_source.h" +#include "capture_setup.h" +#include "audio_input.h" +#include "windowing.h" +#ifdef GSR_APP_AUDIO +#include "../pipewire_audio.h" +#endif + +/* Records video and audio to a file, or to a replay buffer that can be saved to a file at any time */ +typedef struct gsr_recorder gsr_recorder; + +typedef struct { + /* |filepath| is NULL when the recording failed to save */ + void (*replay_saved)(const char *filepath, void *userdata); + void (*recording_started)(const char *filepath, void *userdata); + /* |filepath| is NULL when the recording failed to save */ + void (*recording_stopped)(const char *filepath, void *userdata); + void (*paused_changed)(bool paused, void *userdata); + void *userdata; +} gsr_recorder_callbacks; + +typedef struct { + const gsr_recorder_settings *settings; + gsr_windowing *windowing; + gsr_capture_deps *capture_deps; + gsr_capture_sources *capture_sources; + gsr_audio_input_tracks *audio_input_tracks; + const char **plugin_filepaths; + int num_plugin_filepaths; +#ifdef GSR_APP_AUDIO + gsr_pipewire_audio *pipewire_audio; +#endif +} gsr_recorder_params; + +/* Returns NULL on failure and sets |error| to a |gsr_error| value */ +gsr_recorder* gsr_recorder_create(const gsr_recorder_params *params, const gsr_recorder_callbacks *callbacks, int *error); +/* This is only called when the program is exiting, so memory that the operating system frees on exit isn't free'd. Only set exiting to true if the program is exiting */ +void gsr_recorder_destroy(gsr_recorder *self, bool exiting); + +/* Returns a |gsr_error| value. Records until gsr_recorder_stop is called or until the capture target is gone */ +int gsr_recorder_run(gsr_recorder *self); + +/* These are safe to call from a signal handler or from another thread */ +void gsr_recorder_stop(gsr_recorder *self); +void gsr_recorder_toggle_pause(gsr_recorder *self); +/* Does nothing when the recording is already paused/unpaused */ +void gsr_recorder_set_paused(gsr_recorder *self, bool paused); +void gsr_recorder_toggle_replay_recording(gsr_recorder *self); +/* Does nothing when a recording is already running */ +void gsr_recorder_start_replay_recording(gsr_recorder *self); +/* Does nothing when no recording is running */ +void gsr_recorder_stop_replay_recording(gsr_recorder *self); +bool gsr_recorder_is_replay_recording(const gsr_recorder *self); +#define GSR_RESTART_REPLAY_USE_OPTION -1 +#define GSR_RESTART_REPLAY_DISABLE 0 +#define GSR_RESTART_REPLAY_ENABLE 1 + +/* + |seconds| can be GSR_SAVE_REPLAY_SECONDS_FULL to save the whole replay buffer. + |restart_replay| overrides the -restart-replay-on-save option for this save when it's not GSR_RESTART_REPLAY_USE_OPTION. +*/ +void gsr_recorder_save_replay(gsr_recorder *self, int seconds, int restart_replay); + +#endif /* GSR_RECORDER_RECORDER_H */ diff --git a/include/recorder/recording_clock.h b/include/recorder/recording_clock.h new file mode 100644 index 0000000..b99e936 --- /dev/null +++ b/include/recorder/recording_clock.h @@ -0,0 +1,21 @@ +#ifndef GSR_RECORDER_RECORDING_CLOCK_H +#define GSR_RECORDER_RECORDING_CLOCK_H + +#include <stdbool.h> + +/* Monotonic clock that excludes the time the recording has been paused. Safe to use from multiple threads */ +typedef struct gsr_recording_clock gsr_recording_clock; + +gsr_recording_clock* gsr_recording_clock_create(void); +void gsr_recording_clock_destroy(gsr_recording_clock *self); + +/* Sets the time the recording started to now */ +void gsr_recording_clock_start(gsr_recording_clock *self); +double gsr_recording_clock_get_start_time(const gsr_recording_clock *self); +/* Returns the current time, excluding the time the recording has been paused */ +double gsr_recording_clock_get_time(const gsr_recording_clock *self); + +void gsr_recording_clock_set_paused(gsr_recording_clock *self, bool paused); +bool gsr_recording_clock_is_paused(const gsr_recording_clock *self); + +#endif /* GSR_RECORDER_RECORDING_CLOCK_H */ diff --git a/include/recorder/replay_save.h b/include/recorder/replay_save.h new file mode 100644 index 0000000..7f48851 --- /dev/null +++ b/include/recorder/replay_save.h @@ -0,0 +1,53 @@ +#ifndef GSR_RECORDER_REPLAY_SAVE_H +#define GSR_RECORDER_REPLAY_SAVE_H + +#include <stdbool.h> +#include <stddef.h> +#include <limits.h> +#include <pthread.h> +#include <stdatomic.h> +#include "muxer.h" +#include "audio_capture.h" +#include "capture_setup.h" +#include "settings.h" + +#include "../encoder/encoder.h" +#include "../replay_buffer/replay_buffer.h" + +#define GSR_SAVE_REPLAY_SECONDS_FULL -1 + +typedef struct { + int64_t pts_offset; + int stream_index; +} gsr_audio_pts_offset; + +/* Saves the replay buffer to a file on a separate thread */ +typedef struct { + pthread_t thread; + bool thread_created; + atomic_int finished; + bool success; + char output_filepath[PATH_MAX]; + + AVCodecContext *video_codec_context; + int video_stream_index; + gsr_recording_output recording_output; + gsr_replay_buffer_iterator video_start_iterator; + int64_t video_pts_offset; + gsr_audio_pts_offset *audio_pts_offsets; + size_t num_audio_pts_offsets; + gsr_replay_buffer *cloned_replay_buffer; + gsr_encoder *encoder; +} gsr_replay_save; + +void gsr_replay_save_init(gsr_replay_save *self); +bool gsr_replay_save_is_running(const gsr_replay_save *self); + +/* Returns false if the replay failed to start. |current_save_replay_seconds| can be GSR_SAVE_REPLAY_SECONDS_FULL to save the whole replay buffer */ +bool gsr_replay_save_start(gsr_replay_save *self, AVCodecContext *video_codec_context, int video_stream_index, const gsr_audio_capture *audio_capture, gsr_encoder *encoder, const gsr_recorder_settings *settings, const char *file_extension, bool hdr, gsr_video_sources *video_sources, int current_save_replay_seconds); +/* Returns true when the replay finished saving, in which case |success| and |output_filepath| are set. |output_filepath| is empty when nothing was saved */ +bool gsr_replay_save_poll(gsr_replay_save *self, bool *success, const char **output_filepath); +/* Waits for an ongoing replay save to finish. Returns the same values as gsr_replay_save_poll */ +bool gsr_replay_save_join(gsr_replay_save *self, bool *success, const char **output_filepath); + +#endif /* GSR_RECORDER_REPLAY_SAVE_H */ diff --git a/include/recorder/screenshot.h b/include/recorder/screenshot.h new file mode 100644 index 0000000..00a7889 --- /dev/null +++ b/include/recorder/screenshot.h @@ -0,0 +1,36 @@ +#ifndef GSR_RECORDER_SCREENSHOT_H +#define GSR_RECORDER_SCREENSHOT_H + +#include <stdbool.h> +#include <stdatomic.h> +#include "../egl.h" +#include "../image_writer.h" +#include "../plugins.h" +#include "capture_source.h" +#include "capture_setup.h" +#include "settings.h" + +typedef struct { + const gsr_recorder_settings *settings; + gsr_egl *egl; + gsr_window *window; + gsr_capture_deps *capture_deps; + gsr_capture_sources *capture_sources; + gsr_image_format image_format; + const char **plugin_filepaths; + int num_plugin_filepaths; + const atomic_int *running; + void (*screenshot_saved)(const char *filepath, void *userdata); + void *userdata; +} gsr_screenshot_params; + +bool get_image_format_from_filename(const char *filename, gsr_image_format *image_format); +gsr_color_range image_format_to_color_range(gsr_image_format image_format, int image_quality); +int video_quality_to_image_quality_value(gsr_video_quality video_quality); + +/* Returns a |gsr_error| value. Captures one frame and writes it to |settings->filename| */ +int gsr_screenshot_take(const gsr_screenshot_params *params); + +int gsr_load_plugins(gsr_plugins *plugins, const char **plugin_filepaths, int num_plugin_filepaths, const gsr_recorder_settings *settings, gsr_egl *egl, vec2i video_size); + +#endif /* GSR_RECORDER_SCREENSHOT_H */ diff --git a/include/recorder/settings.h b/include/recorder/settings.h new file mode 100644 index 0000000..164e082 --- /dev/null +++ b/include/recorder/settings.h @@ -0,0 +1,55 @@ +#ifndef GSR_RECORDER_SETTINGS_H +#define GSR_RECORDER_SETTINGS_H + +#include <stdbool.h> +#include <stdint.h> +#include "../defs.h" +#include "../vec2.h" + +typedef struct { + gsr_video_encoder_hardware video_encoder; + gsr_pixel_format pixel_format; + gsr_framerate_mode framerate_mode; + gsr_color_range color_range; + gsr_tune tune; + gsr_video_codec video_codec; + gsr_audio_codec audio_codec; + gsr_bitrate_mode bitrate_mode; + gsr_video_quality video_quality; + gsr_replay_storage replay_storage; + + const char *capture_source; + const char *container_format; + const char *filename; + const char *replay_recording_directory; + const char *portal_session_token_filepath; + const char *recording_saved_script; + const char *ffmpeg_opts; + const char *ffmpeg_video_opts; + const char *ffmpeg_audio_opts; + bool verbose; + bool gl_debug; + bool fallback_cpu_encoding; + bool low_power; + bool exclude_metadata; + bool record_cursor; + bool date_folders; + bool restore_portal_session; + bool restart_replay_on_save; + bool write_first_frame_ts; + bool is_replaying; + bool is_livestream; + bool is_output_piped; + bool low_latency_recording; + bool very_old_gpu; + int64_t video_bitrate; + int64_t audio_bitrate; + int64_t fps; + int64_t replay_buffer_size_secs; + double keyint; + vec2i output_resolution; + vec2i region_size; + vec2i region_position; +} gsr_recorder_settings; + +#endif /* GSR_RECORDER_SETTINGS_H */ diff --git a/include/recorder/video_codec.h b/include/recorder/video_codec.h new file mode 100644 index 0000000..9347a9e --- /dev/null +++ b/include/recorder/video_codec.h @@ -0,0 +1,18 @@ +#ifndef GSR_RECORDER_VIDEO_CODEC_H +#define GSR_RECORDER_VIDEO_CODEC_H + +#include <stdbool.h> +#include "../defs.h" +#include "../egl.h" +#include "settings.h" + +#include <libavcodec/avcodec.h> + +int video_quality_to_h264_equivalent_qp(gsr_video_quality video_quality); +enum AVPixelFormat get_pixel_format(gsr_video_codec video_codec, gsr_gpu_vendor vendor, bool use_software_video_encoder); + +AVCodecContext* create_video_codec_context(enum AVPixelFormat pix_fmt, const AVCodec *codec, const gsr_egl *egl, const gsr_recorder_settings *settings, int width, int height); +bool open_video_software(AVCodecContext *codec_context, const gsr_recorder_settings *settings); +bool open_video_hardware(AVCodecContext *codec_context, bool low_power, const gsr_egl *egl, const gsr_recorder_settings *settings); + +#endif /* GSR_RECORDER_VIDEO_CODEC_H */ diff --git a/include/recorder/windowing.h b/include/recorder/windowing.h new file mode 100644 index 0000000..630f3fe --- /dev/null +++ b/include/recorder/windowing.h @@ -0,0 +1,35 @@ +#ifndef GSR_RECORDER_WINDOWING_H +#define GSR_RECORDER_WINDOWING_H + +#include <stdbool.h> +#include "../defs.h" +#include "../egl.h" + +#include <X11/Xlib.h> + +typedef struct { + bool monitor_capture; + bool gl_debug; + bool listen_to_x11_events; +} gsr_windowing_params; + +typedef struct { + Display *display; + gsr_window *window; + gsr_egl egl; + bool egl_loaded; + bool card_path_found; +} gsr_windowing; + +/* Returns a |gsr_error| value. Connects to the display server and creates a window */ +int gsr_windowing_init(gsr_windowing *self, const gsr_windowing_params *params); +/* Returns a |gsr_error| value. Loads opengl and finds the drm card to use */ +int gsr_windowing_load_egl(gsr_windowing *self, const gsr_windowing_params *params); +void gsr_windowing_deinit(gsr_windowing *self); +bool gsr_windowing_is_wayland(const gsr_windowing *self); + +bool gsr_windowing_is_using_prime_run(void); +void gsr_windowing_disable_prime_run(void); +bool monitor_capture_use_drm(const gsr_window *window, gsr_gpu_vendor vendor); + +#endif /* GSR_RECORDER_WINDOWING_H */ diff --git a/include/replay_buffer/replay_buffer.h b/include/replay_buffer/replay_buffer.h index 6e91103..bf2e4e6 100644 --- a/include/replay_buffer/replay_buffer.h +++ b/include/replay_buffer/replay_buffer.h @@ -14,6 +14,7 @@ typedef struct { struct gsr_replay_buffer { void (*destroy)(gsr_replay_buffer *self); + void (*destroy_at_exit)(gsr_replay_buffer *self); bool (*append)(gsr_replay_buffer *self, const AVPacket *av_packet, double timestamp); void (*clear)(gsr_replay_buffer *self); AVPacket* (*iterator_get_packet)(gsr_replay_buffer *self, gsr_replay_buffer_iterator iterator); @@ -30,6 +31,13 @@ struct gsr_replay_buffer { gsr_replay_buffer* gsr_replay_buffer_create(gsr_replay_storage replay_storage, const char *replay_directory, double replay_buffer_time, size_t replay_buffer_num_packets); void gsr_replay_buffer_destroy(gsr_replay_buffer *self); +/* + Only frees the resources that the operating system doesn't free when the process exits, such as files, + and leaves the memory to the operating system to free. Freeing a replay buffer that holds several gigabytes + of data takes seconds, which is time wasted when the process is exiting anyway. + |self| can't be used after this. +*/ +void gsr_replay_buffer_destroy_at_exit(gsr_replay_buffer *self); bool gsr_replay_buffer_append(gsr_replay_buffer *self, const AVPacket *av_packet, double timestamp); void gsr_replay_buffer_clear(gsr_replay_buffer *self); diff --git a/include/sound.h b/include/sound.h new file mode 100644 index 0000000..31c7d94 --- /dev/null +++ b/include/sound.h @@ -0,0 +1,59 @@ +#ifndef GSR_SOUND_H +#define GSR_SOUND_H + +#include <stdbool.h> +#include <stddef.h> + +typedef struct { + void *handle; + unsigned int frames; +} SoundDevice; + +typedef struct { + char name[256]; + char description[256]; +} gsr_audio_device; + +typedef struct { + char default_output[256]; + char default_input[256]; + gsr_audio_device *items; + size_t num_items; + size_t capacity_items; +} gsr_audio_devices; + +typedef enum { + GSR_AUDIO_FORMAT_S16, + GSR_AUDIO_FORMAT_S32, + GSR_AUDIO_FORMAT_F32 +} gsr_audio_format; + +/* + Get a sound device by name, returning the device into the |device| parameter. + |device_name| can be a device name or "default_output", "default_input" or "" to not connect to any device (used for app audio for example). + If the device name is "default_output" or "default_input" then it will automatically switch which + device is records from when the default output/input is changed in the system audio settings. + Returns 0 on success, or a negative value on failure. +*/ +int sound_device_get_by_name(SoundDevice *device, const char *node_name, const char *device_name, const char *description, unsigned int num_channels, unsigned int period_frame_size, gsr_audio_format audio_format); + +void sound_device_close(SoundDevice *device); + +/* + Discards the audio that has been captured so far. + Call this before the first call to sound_device_read_next_chunk to not get audio that was captured before that point, + since the sound device can be created a while before audio capture starts. +*/ +void sound_device_flush(SoundDevice *device); + +/* + Returns the next chunk of audio into @buffer. + Returns the number of frames read, or a negative value on failure. +*/ +int sound_device_read_next_chunk(SoundDevice *device, void **buffer, double timeout_sec, double *latency_seconds); + +void get_pulseaudio_inputs(gsr_audio_devices *audio_devices); +void gsr_audio_devices_deinit(gsr_audio_devices *self); +bool pulseaudio_server_is_pipewire(void); + +#endif /* GSR_SOUND_H */ diff --git a/include/sound.hpp b/include/sound.hpp deleted file mode 100644 index d78e4b6..0000000 --- a/include/sound.hpp +++ /dev/null @@ -1,91 +0,0 @@ -/* - Copyright (C) 2020 dec05eba - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see <https://www.gnu.org/licenses/>. -*/ - -#ifndef GPU_SCREEN_RECORDER_H -#define GPU_SCREEN_RECORDER_H - -#include <optional> -#include <vector> -#include <string> - -typedef struct { - void *handle; - unsigned int frames; -} SoundDevice; - -struct AudioDevice { - std::string name; - std::string description; -}; - -struct AudioDevices { - std::string default_output; - std::string default_input; - std::vector<AudioDevice> audio_inputs; -}; - -enum class AudioInputType { - DEVICE, - APPLICATION -}; - -struct AudioInput { - std::string name; - AudioInputType type = AudioInputType::DEVICE; - bool inverted = false; -}; - -struct MergedAudioInputs { - std::optional<std::string> custom_name; - std::string track_name; - std::vector<AudioInput> audio_inputs; -}; - -typedef enum { - S16, - S32, - F32 -} AudioFormat; - -/* - Get a sound device by name, returning the device into the |device| parameter. - |device_name| can be a device name or "default_output", "default_input" or "" to not connect to any device (used for app audio for example). - If the device name is "default_output" or "default_input" then it will automatically switch which - device is records from when the default output/input is changed in the system audio settings. - Returns 0 on success, or a negative value on failure. -*/ -int sound_device_get_by_name(SoundDevice *device, const char *node_name, const char *device_name, const char *description, unsigned int num_channels, unsigned int period_frame_size, AudioFormat audio_format); - -void sound_device_close(SoundDevice *device); - -/* - Discards the audio that has been captured so far. - Call this before the first call to sound_device_read_next_chunk to not get audio that was captured before that point, - since the sound device can be created a while before audio capture starts. -*/ -void sound_device_flush(SoundDevice *device); - -/* - Returns the next chunk of audio into @buffer. - Returns the number of frames read, or a negative value on failure. -*/ -int sound_device_read_next_chunk(SoundDevice *device, void **buffer, double timeout_sec, double *latency_seconds); - -AudioDevices get_pulseaudio_inputs(); -bool pulseaudio_server_is_pipewire(); - -#endif /* GPU_SCREEN_RECORDER_H */ diff --git a/include/utils.h b/include/utils.h index d2c3b7d..d1a2a3c 100644 --- a/include/utils.h +++ b/include/utils.h @@ -6,6 +6,7 @@ #include "../include/defs.h" #include <stdbool.h> #include <stdint.h> +#include <stddef.h> typedef struct AVCodecContext AVCodecContext; typedef struct AVFrame AVFrame; @@ -64,4 +65,21 @@ unsigned int gl_create_texture(gsr_egl *egl, int width, int height, int internal bool get_nvidia_driver_version(int *major, int *minor); +/* Return false from the callback to stop splitting */ +typedef bool (*gsr_string_split_callback)(const char *str, size_t size, void *userdata); +void gsr_string_split(const char *str, char delimiter, gsr_string_split_callback callback, void *userdata); + +bool gsr_string_starts_with(const char *str, size_t str_size, const char *substr); +bool gsr_string_ends_with(const char *str, const char *substr); + +bool gsr_string_to_int(const char *str, size_t size, int *number); +bool gsr_string_to_int64(const char *str, size_t size, int64_t *number); + +bool gsr_array_ensure_capacity(void **array, size_t num_items, size_t *capacity_items, size_t item_size); + +/* These write a formatted local date/time into |str|, which should be at least 128 bytes in size */ +void gsr_get_date_str(char *str, size_t size); +void gsr_get_date_only_str(char *str, size_t size); +void gsr_get_time_only_str(char *str, size_t size); + #endif /* GSR_UTILS_H */ diff --git a/kms/client/kms_client.c b/kms/client/kms_client.c index 0d4d963..c0279d2 100644 --- a/kms/client/kms_client.c +++ b/kms/client/kms_client.c @@ -1,4 +1,5 @@ #include "kms_client.h" +#include "../../include/log.h" #include "../../include/utils.h" #include <stdio.h> #include <string.h> @@ -9,10 +10,10 @@ #include <stdbool.h> #include <fcntl.h> #include <sys/socket.h> +#include <sys/stat.h> #include <sys/un.h> #include <sys/wait.h> #include <poll.h> -#include <sys/stat.h> #ifdef __linux__ #include <sys/capability.h> #endif @@ -225,14 +226,14 @@ int gsr_kms_client_init(gsr_kms_client *self, const char *card_path) { struct sockaddr_un remote_addr = {0}; if(!create_socket_path(self->initial_socket_path, sizeof(self->initial_socket_path))) { - fprintf(stderr, "gsr error: gsr_kms_client_init: failed to create path to kms socket\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_kms_client_init: failed to create path to kms socket"); return -1; } char server_filepath[PATH_MAX]; #ifdef __linux__ if(!readlink_realpath("/proc/self/exe", server_filepath)) { - fprintf(stderr, "gsr error: gsr_kms_client_init: failed to resolve /proc/self/exe\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_kms_client_init: failed to resolve /proc/self/exe"); return -1; } @@ -241,7 +242,7 @@ int gsr_kms_client_init(gsr_kms_client *self, const char *card_path) { size_t size = PATH_MAX; if (sysctl(mib, 4, server_filepath, &size, NULL, 0) != 0) { - fprintf(stderr, "gsr error: gsr_kms_client_init: failed to resolve pathname using sysctl\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_kms_client_init: failed to resolve pathname using sysctl"); return -1; } @@ -251,19 +252,19 @@ int gsr_kms_client_init(gsr_kms_client *self, const char *card_path) { file_get_directory(server_filepath); if(!strcat_safe(server_filepath, sizeof(server_filepath), "/gsr-kms-server")) { - fprintf(stderr, "gsr error: gsr_kms_client_init: gsr-kms-server path too long\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_kms_client_init: gsr-kms-server path too long"); return -1; } if(access(server_filepath, F_OK) != 0) { - fprintf(stderr, "gsr info: gsr_kms_client_init: gsr-kms-server is not installed in the same directory as gpu-screen-recorder (%s not found), looking for gsr-kms-server in PATH instead\n", server_filepath); + gsr_log(GSR_LOG_LEVEL_INFO, "gsr_kms_client_init: gsr-kms-server is not installed in the same directory as gpu-screen-recorder (%s not found), looking for gsr-kms-server in PATH instead", server_filepath); if(!find_program_in_path("gsr-kms-server", server_filepath, sizeof(server_filepath)) || access(server_filepath, F_OK) != 0) { - fprintf(stderr, "gsr error: gsr_kms_client_init: gsr-kms-server was not found in PATH. Please install gpu-screen-recorder properly\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_kms_client_init: gsr-kms-server was not found in PATH. Please install gpu-screen-recorder properly"); return -1; } } - fprintf(stderr, "gsr info: gsr_kms_client_init: setting up connection to %s\n", server_filepath); + gsr_log(GSR_LOG_LEVEL_INFO, "gsr_kms_client_init: setting up connection to %s", server_filepath); const bool inside_flatpak = getenv("FLATPAK_ID") != NULL; const char *home = getenv("HOME"); @@ -288,23 +289,23 @@ int gsr_kms_client_init(gsr_kms_client *self, const char *card_path) { cap_free(kms_server_cap); } else if(!inside_flatpak) { if(errno == ENODATA) - fprintf(stderr, "gsr info: gsr_kms_client_init: gsr-kms-server is missing sys_admin cap and will require root authentication. To bypass this automatically, run: sudo setcap cap_sys_admin+ep '%s'\n", server_filepath); + gsr_log(GSR_LOG_LEVEL_INFO, "gsr_kms_client_init: gsr-kms-server is missing sys_admin cap and will require root authentication. To bypass this automatically, run: sudo setcap cap_sys_admin+ep '%s'", server_filepath); else - fprintf(stderr, "gsr info: gsr_kms_client_init: failed to get cap\n"); + gsr_log(GSR_LOG_LEVEL_INFO, "gsr_kms_client_init: failed to get cap"); } #else - fprintf(stderr, "gsr info: gsr_kms_client_init: platform doesn't support cap\n"); + gsr_log(GSR_LOG_LEVEL_INFO, "gsr_kms_client_init: platform doesn't support cap"); #endif } - if(socketpair(AF_UNIX, SOCK_STREAM, 0, self->socket_pair) == -1) { - fprintf(stderr, "gsr error: gsr_kms_client_init: socketpair failed, error: %s\n", strerror(errno)); + if(socketpair(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0, self->socket_pair) == -1) { + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_kms_client_init: socketpair failed, error: %s", strerror(errno)); goto err; } - self->initial_socket_fd = socket(AF_UNIX, SOCK_STREAM, 0); + self->initial_socket_fd = socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0); if(self->initial_socket_fd == -1) { - fprintf(stderr, "gsr error: gsr_kms_client_init: socket failed, error: %s\n", strerror(errno)); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_kms_client_init: socket failed, error: %s", strerror(errno)); goto err; } @@ -316,18 +317,18 @@ int gsr_kms_client_init(gsr_kms_client *self, const char *card_path) { umask(prev_mask); if(bind_res == -1) { - fprintf(stderr, "gsr error: gsr_kms_client_init: failed to bind socket, error: %s\n", strerror(errno)); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_kms_client_init: failed to bind socket, error: %s", strerror(errno)); goto err; } if(listen(self->initial_socket_fd, 1) == -1) { - fprintf(stderr, "gsr error: gsr_kms_client_init: failed to listen on socket, error: %s\n", strerror(errno)); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_kms_client_init: failed to listen on socket, error: %s", strerror(errno)); goto err; } pid_t pid = fork(); if(pid == -1) { - fprintf(stderr, "gsr error: gsr_kms_client_init: fork failed, error: %s\n", strerror(errno)); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_kms_client_init: fork failed, error: %s", strerror(errno)); goto err; } else if(pid == 0) { /* child */ if(inside_flatpak) { @@ -340,7 +341,7 @@ int gsr_kms_client_init(gsr_kms_client *self, const char *card_path) { const char *args[] = { "pkexec", server_filepath, self->initial_socket_path, card_path, NULL }; execvp(args[0], (char *const*)args); } - fprintf(stderr, "gsr error: gsr_kms_client_init: failed to launch \"gsr-kms-server\", error: %s\n", strerror(errno)); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_kms_client_init: failed to launch \"gsr-kms-server\", error: %s", strerror(errno)); _exit(127); } else { /* parent */ self->kms_server_pid = pid; @@ -348,7 +349,7 @@ int gsr_kms_client_init(gsr_kms_client *self, const char *card_path) { // We need this dumb-shit retardation with unix domain socket and then replace it with socketpair because // pkexec doesn't work with socketpair................ - fprintf(stderr, "gsr info: gsr_kms_client_init: waiting for server to connect\n"); + gsr_log(GSR_LOG_LEVEL_INFO, "gsr_kms_client_init: waiting for server to connect"); struct pollfd poll_fd = { .fd = self->initial_socket_fd, .events = POLLIN, @@ -360,7 +361,7 @@ int gsr_kms_client_init(gsr_kms_client *self, const char *card_path) { socklen_t sock_len = 0; self->initial_client_fd = accept(self->initial_socket_fd, (struct sockaddr*)&remote_addr, &sock_len); if(self->initial_client_fd == -1) { - fprintf(stderr, "gsr error: gsr_kms_client_init: accept failed on socket, error: %s\n", strerror(errno)); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_kms_client_init: accept failed on socket, error: %s", strerror(errno)); goto err; } break; @@ -371,7 +372,7 @@ int gsr_kms_client_init(gsr_kms_client *self, const char *card_path) { int exit_code = -1; if(WIFEXITED(status)) exit_code = WEXITSTATUS(status); - fprintf(stderr, "gsr error: gsr_kms_client_init: kms server died or never started, exit code: %d\n", exit_code); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_kms_client_init: kms server died or never started, exit code: %d", exit_code); self->kms_server_pid = -1; if(exit_code != 0) result = exit_code; @@ -380,14 +381,14 @@ int gsr_kms_client_init(gsr_kms_client *self, const char *card_path) { } poll_fd.revents = 0; } - fprintf(stderr, "gsr info: gsr_kms_client_init: server connected\n"); + gsr_log(GSR_LOG_LEVEL_INFO, "gsr_kms_client_init: server connected"); - fprintf(stderr, "gsr info: replacing file-backed unix domain socket with socketpair\n"); + gsr_log(GSR_LOG_LEVEL_INFO, "replacing file-backed unix domain socket with socketpair"); if(gsr_kms_client_replace_connection(self) != 0) goto err; cleanup_socket(self, false); - fprintf(stderr, "gsr info: using socketpair\n"); + gsr_log(GSR_LOG_LEVEL_INFO, "using socketpair"); return 0; @@ -445,21 +446,21 @@ int gsr_kms_client_replace_connection(gsr_kms_client *self) { request.type = KMS_REQUEST_TYPE_REPLACE_CONNECTION; request.new_connection_fd = self->socket_pair[GSR_SOCKET_PAIR_REMOTE]; if(send_msg_to_server(self->initial_client_fd, &request) == -1) { - fprintf(stderr, "gsr error: gsr_kms_client_replace_connection: failed to send request message to server\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_kms_client_replace_connection: failed to send request message to server"); return -1; } const int recv_res = recv_msg_from_server(self->kms_server_pid, self->socket_pair[GSR_SOCKET_PAIR_LOCAL], &response); if(recv_res == 0) { - fprintf(stderr, "gsr warning: gsr_kms_client_replace_connection: kms server shut down\n"); + gsr_log(GSR_LOG_LEVEL_WARNING, "gsr_kms_client_replace_connection: kms server shut down"); return -1; } else if(recv_res == -1) { - fprintf(stderr, "gsr error: gsr_kms_client_replace_connection: failed to receive response\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_kms_client_replace_connection: failed to receive response"); return -1; } if(response.version != GSR_KMS_PROTOCOL_VERSION) { - fprintf(stderr, "gsr error: gsr_kms_client_replace_connection: expected gsr-kms-server protocol version to be %u, but it's %u. please reinstall gpu screen recorder\n", GSR_KMS_PROTOCOL_VERSION, response.version); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_kms_client_replace_connection: expected gsr-kms-server protocol version to be %u, but it's %u. please reinstall gpu screen recorder", GSR_KMS_PROTOCOL_VERSION, response.version); /*close_fds(response);*/ return -1; } @@ -478,24 +479,24 @@ int gsr_kms_client_get_kms(gsr_kms_client *self, gsr_kms_response *response) { request.type = KMS_REQUEST_TYPE_GET_KMS; request.new_connection_fd = 0; if(send_msg_to_server(self->socket_pair[GSR_SOCKET_PAIR_LOCAL], &request) == -1) { - fprintf(stderr, "gsr error: gsr_kms_client_get_kms: failed to send request message to server\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_kms_client_get_kms: failed to send request message to server"); strcpy(response->err_msg, "failed to send"); return -1; } const int recv_res = recv_msg_from_server(self->kms_server_pid, self->socket_pair[GSR_SOCKET_PAIR_LOCAL], response); if(recv_res == 0) { - fprintf(stderr, "gsr warning: gsr_kms_client_get_kms: kms server shut down\n"); + gsr_log(GSR_LOG_LEVEL_WARNING, "gsr_kms_client_get_kms: kms server shut down"); strcpy(response->err_msg, "failed to receive"); return -1; } else if(recv_res == -1) { - fprintf(stderr, "gsr error: gsr_kms_client_get_kms: failed to receive response\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_kms_client_get_kms: failed to receive response"); strcpy(response->err_msg, "failed to receive"); return -1; } if(response->version != GSR_KMS_PROTOCOL_VERSION) { - fprintf(stderr, "gsr error: gsr_kms_client_get_kms: expected gsr-kms-server protocol version to be %u, but it's %u. please reinstall gpu screen recorder\n", GSR_KMS_PROTOCOL_VERSION, response->version); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_kms_client_get_kms: expected gsr-kms-server protocol version to be %u, but it's %u. please reinstall gpu screen recorder", GSR_KMS_PROTOCOL_VERSION, response->version); /*close_fds(response);*/ strcpy(response->err_msg, "mismatching protocol version"); return -1; diff --git a/kms/server/kms_server.c b/kms/server/kms_server.c index c41f8bd..4d9171f 100644 --- a/kms/server/kms_server.c +++ b/kms/server/kms_server.c @@ -509,7 +509,7 @@ int main(int argc, char **argv) { } const char *domain_socket_path = argv[1]; - socket_fd = socket(AF_UNIX, SOCK_STREAM, 0); + socket_fd = socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0); if(socket_fd == -1) { fprintf(stderr, "kms server error: failed to create socket, error: %s\n", strerror(errno)); return 2; diff --git a/meson.build b/meson.build index 4a79378..5a514fe 100644 --- a/meson.build +++ b/meson.build @@ -1,10 +1,10 @@ -project('gpu-screen-recorder', ['c', 'cpp'], version : '5.15.3', default_options : ['warning_level=2', 'cpp_std=c++17']) +project('gpu-screen-recorder', 'c', version : '6.0.0', default_options : ['c_std=gnu11', 'warning_level=2']) -add_project_arguments('-Wshadow', language : ['c', 'cpp']) +add_project_arguments('-Wshadow', language : 'c') if get_option('buildtype') == 'debug' - add_project_arguments('-g3', language : ['c', 'cpp']) + add_project_arguments('-g3', language : 'c') elif get_option('buildtype') == 'release' - add_project_arguments('-DNDEBUG', language : ['c', 'cpp']) + add_project_arguments('-DNDEBUG', language : 'c') endif src = [ @@ -30,6 +30,24 @@ src = [ 'src/replay_buffer/replay_buffer.c', 'src/replay_buffer/replay_buffer_ram.c', 'src/replay_buffer/replay_buffer_disk.c', + 'src/log.c', + 'src/json.c', + 'src/ffmpeg_utils.c', + 'src/recorder/audio_codec.c', + 'src/recorder/video_codec.c', + 'src/recorder/codec_select.c', + 'src/recorder/windowing.c', + 'src/recorder/capture_source.c', + 'src/recorder/capture_setup.c', + 'src/recorder/audio_input.c', + 'src/recorder/audio_capture.c', + 'src/recorder/recording_clock.c', + 'src/recorder/muxer.c', + 'src/recorder/replay_save.c', + 'src/recorder/screenshot.c', + 'src/recorder/recorder.c', + 'src/cli/commands.c', + 'src/cli/ipc.c', 'src/egl.c', 'src/cuda.c', 'src/window_texture.c', @@ -45,8 +63,8 @@ src = [ 'src/defs.c', 'src/plugins.c', 'src/wayland_host_bridge.c', - 'src/sound.cpp', - 'src/main.cpp', + 'src/sound.c', + 'src/cli/main.c', ] subdir('protocol') @@ -55,6 +73,11 @@ src += protocol_src cc = meson.get_compiler('c') m_dep = cc.find_library('m', required : true) +# Overrides the libav* dependencies below with a statically linked ffmpeg built from source. +if get_option('ffmpeg_static') == true + subproject('ffmpeg') +endif + dep = [ dependency('threads'), m_dep, @@ -90,7 +113,7 @@ if get_option('portal') == true 'src/dbus.c', 'src/pipewire_video.c', ] - add_project_arguments('-DGSR_PORTAL', language : ['c', 'cpp']) + add_project_arguments('-DGSR_PORTAL', language : 'c') uses_pipewire = true endif @@ -98,7 +121,7 @@ if get_option('app_audio') == true src += [ 'src/pipewire_audio.c', ] - add_project_arguments('-DGSR_APP_AUDIO', language : ['c', 'cpp']) + add_project_arguments('-DGSR_APP_AUDIO', language : 'c') uses_pipewire = true endif @@ -108,16 +131,17 @@ if uses_pipewire == true dependency('libspa-0.2'), dependency('dbus-1'), ] - add_project_arguments('-DGSR_DBUS', language : ['c', 'cpp']) + add_project_arguments('-DGSR_DBUS', language : 'c') endif -add_project_arguments('-DGSR_VERSION="' + meson.project_version() + '"', language: ['c', 'cpp']) +add_project_arguments('-DGSR_VERSION="' + meson.project_version() + '"', language: 'c') executable('gsr-kms-server', 'kms/server/kms_server.c', dependencies : dependency('libdrm'), c_args : '-fstack-protector-all', install : true) +executable('gsr-cli', ['tools/gsr-cli/main.c', 'src/json.c', 'src/log.c'], install : true) executable('gpu-screen-recorder', src, dependencies : dep, install : true) install_headers('plugin/plugin.h', install_dir : 'include/gsr') -install_man('gpu-screen-recorder.1', 'gsr-kms-server.1') +install_man('gpu-screen-recorder.1', 'gsr-cli.1', 'gsr-kms-server.1') install_subdir('scripts', install_dir: 'share/gpu-screen-recorder') if get_option('systemd') == true diff --git a/meson_options.txt b/meson_options.txt index d50e2db..23751b6 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -4,3 +4,4 @@ option('nvidia_suspend_fix', type : 'boolean', value : true, description : 'Inst option('portal', type : 'boolean', value : true, description : 'Build with support for xdg desktop portal ScreenCast capture (wayland only) (-w portal option). Requires pipewire') option('app_audio', type : 'boolean', value : true, description : 'Build with support for recording a single audio source (-a app: option). Requires pipewire') option('plugin_examples', type : 'boolean', value : false, description : 'Build plugin examples') +option('ffmpeg_static', type : 'boolean', value : false, description : 'Download and build a minimal ffmpeg and the libraries it depends on from source with lto and link them statically into gpu-screen-recorder instead of using the system ffmpeg. Requires make, nasm, cmake and perl') diff --git a/project.conf b/project.conf index 0e0ed9c..47e993b 100644 --- a/project.conf +++ b/project.conf @@ -1,14 +1,11 @@ [package] name = "gpu-screen-recorder" type = "executable" -version = "5.15.3" +version = "6.0.0" platforms = ["posix"] -[lang.cpp] -version = "c++17" - [config] -ignore_dirs = ["kms/server", "build", "debug-build", "plugin/examples"] +ignore_dirs = ["kms/server", "tools", "build", "debug-build", "plugin/examples"] #error_on_warning = "true" [define] diff --git a/src/args_parser.c b/src/args_parser.c index c37712a..d10da67 100644 --- a/src/args_parser.c +++ b/src/args_parser.c @@ -1,4 +1,5 @@ #include "../include/args_parser.h" +#include "../include/log.h" #include "../include/defs.h" #include "../include/egl.h" #include "../include/window/window.h" @@ -116,17 +117,18 @@ static bool arg_get_enum_value_by_name(const Arg *arg, const char *name, int *en return false; } -static void arg_print_expected_enum_names(const Arg *arg) { +static void arg_get_expected_enum_names(const Arg *arg, char *buf, size_t buf_size) { assert(arg->type == ARG_TYPE_ENUM); assert(arg->enum_values); + buf[0] = '\0'; + size_t offset = 0; for(int i = 0; i < arg->num_enum_values; ++i) { - if(i > 0) { - if(i == arg->num_enum_values -1) - fprintf(stderr, " or "); - else - fprintf(stderr, ", "); - } - fprintf(stderr, "'%s'", arg->enum_values[i].name); + const char *separator = ""; + if(i > 0) + separator = i == arg->num_enum_values - 1 ? " or " : ", "; + const int written = snprintf(buf + offset, offset < buf_size ? buf_size - offset : 0, "%s'%s'", separator, arg->enum_values[i].name); + if(written > 0) + offset += written; } } @@ -204,7 +206,7 @@ static void usage_header(void) { "[-cursor yes|no] [-keyint <value>] [-restore-portal-session yes|no] [-portal-session-token-filepath filepath] [-encoder gpu|cpu] " "[-fallback-cpu-encoding yes|no] [-o <output_file>] [-ro <output_directory>] [-ffmpeg-opts <options>] [--list-capture-options [card_path]] " "[--list-monitors] [--list-audio-devices] [--list-application-audio] [--list-v4l2-devices] [-write-first-frame-ts yes|no] [-low-power yes|no] " - "[-v yes|no] [-gl-debug yes|no] [-exclude-metadata yes|no] [--version] [-h|--help]\n", program_name); + "[-ipc <socket_path>] [-v yes|no] [-gl-debug yes|no] [-exclude-metadata yes|no] [--version] [-h|--help]\n", program_name); fflush(stdout); } @@ -251,283 +253,283 @@ static bool file_is_pipe_or_char_device(const char *filepath) { } static bool args_parser_set_values(args_parser *self) { - self->video_encoder = (gsr_video_encoder_hardware)args_get_enum_by_key(self->args, NUM_ARGS, "-encoder", GSR_VIDEO_ENCODER_HW_GPU); - self->pixel_format = (gsr_pixel_format)args_get_enum_by_key(self->args, NUM_ARGS, "-pixfmt", GSR_PIXEL_FORMAT_YUV420); - self->framerate_mode = (gsr_framerate_mode)args_get_enum_by_key(self->args, NUM_ARGS, "-fm", GSR_FRAMERATE_MODE_VARIABLE); - self->color_range = (gsr_color_range)args_get_enum_by_key(self->args, NUM_ARGS, "-cr", GSR_COLOR_RANGE_LIMITED); - self->tune = (gsr_tune)args_get_enum_by_key(self->args, NUM_ARGS, "-tune", GSR_TUNE_PERFORMANCE); - self->video_codec = (gsr_video_codec)args_get_enum_by_key(self->args, NUM_ARGS, "-k", GSR_VIDEO_CODEC_AUTO); - self->audio_codec = (gsr_audio_codec)args_get_enum_by_key(self->args, NUM_ARGS, "-ac", GSR_AUDIO_CODEC_OPUS); - self->bitrate_mode = (gsr_bitrate_mode)args_get_enum_by_key(self->args, NUM_ARGS, "-bm", GSR_BITRATE_MODE_AUTO); - self->replay_storage = (gsr_replay_storage)args_get_enum_by_key(self->args, NUM_ARGS, "-replay-storage", GSR_REPLAY_STORAGE_RAM); - - self->capture_source = args_get_value_by_key(self->args, NUM_ARGS, "-w"); - self->verbose = args_get_boolean_by_key(self->args, NUM_ARGS, "-v", true); - self->gl_debug = args_get_boolean_by_key(self->args, NUM_ARGS, "-gl-debug", false); - self->record_cursor = args_get_boolean_by_key(self->args, NUM_ARGS, "-cursor", true); - self->date_folders = args_get_boolean_by_key(self->args, NUM_ARGS, "-df", false); - self->restore_portal_session = args_get_boolean_by_key(self->args, NUM_ARGS, "-restore-portal-session", false); - self->restart_replay_on_save = args_get_boolean_by_key(self->args, NUM_ARGS, "-restart-replay-on-save", false); + self->settings.video_encoder = (gsr_video_encoder_hardware)args_get_enum_by_key(self->args, NUM_ARGS, "-encoder", GSR_VIDEO_ENCODER_HW_GPU); + self->settings.pixel_format = (gsr_pixel_format)args_get_enum_by_key(self->args, NUM_ARGS, "-pixfmt", GSR_PIXEL_FORMAT_YUV420); + self->settings.framerate_mode = (gsr_framerate_mode)args_get_enum_by_key(self->args, NUM_ARGS, "-fm", GSR_FRAMERATE_MODE_VARIABLE); + self->settings.color_range = (gsr_color_range)args_get_enum_by_key(self->args, NUM_ARGS, "-cr", GSR_COLOR_RANGE_LIMITED); + self->settings.tune = (gsr_tune)args_get_enum_by_key(self->args, NUM_ARGS, "-tune", GSR_TUNE_PERFORMANCE); + self->settings.video_codec = (gsr_video_codec)args_get_enum_by_key(self->args, NUM_ARGS, "-k", GSR_VIDEO_CODEC_AUTO); + self->settings.audio_codec = (gsr_audio_codec)args_get_enum_by_key(self->args, NUM_ARGS, "-ac", GSR_AUDIO_CODEC_OPUS); + self->settings.bitrate_mode = (gsr_bitrate_mode)args_get_enum_by_key(self->args, NUM_ARGS, "-bm", GSR_BITRATE_MODE_AUTO); + self->settings.replay_storage = (gsr_replay_storage)args_get_enum_by_key(self->args, NUM_ARGS, "-replay-storage", GSR_REPLAY_STORAGE_RAM); + + self->settings.capture_source = args_get_value_by_key(self->args, NUM_ARGS, "-w"); + self->settings.verbose = args_get_boolean_by_key(self->args, NUM_ARGS, "-v", true); + self->settings.gl_debug = args_get_boolean_by_key(self->args, NUM_ARGS, "-gl-debug", false); + self->settings.record_cursor = args_get_boolean_by_key(self->args, NUM_ARGS, "-cursor", true); + self->settings.date_folders = args_get_boolean_by_key(self->args, NUM_ARGS, "-df", false); + self->settings.restore_portal_session = args_get_boolean_by_key(self->args, NUM_ARGS, "-restore-portal-session", false); + self->settings.restart_replay_on_save = args_get_boolean_by_key(self->args, NUM_ARGS, "-restart-replay-on-save", false); const bool overclock = args_get_boolean_by_key(self->args, NUM_ARGS, "-oc", false); - self->fallback_cpu_encoding = args_get_boolean_by_key(self->args, NUM_ARGS, "-fallback-cpu-encoding", false); - self->write_first_frame_ts = args_get_boolean_by_key(self->args, NUM_ARGS, "-write-first-frame-ts", false); - self->low_power = args_get_boolean_by_key(self->args, NUM_ARGS, "-low-power", false); - self->exclude_metadata = args_get_boolean_by_key(self->args, NUM_ARGS, "-exclude-metadata", false); + self->settings.fallback_cpu_encoding = args_get_boolean_by_key(self->args, NUM_ARGS, "-fallback-cpu-encoding", false); + self->settings.write_first_frame_ts = args_get_boolean_by_key(self->args, NUM_ARGS, "-write-first-frame-ts", false); + self->settings.low_power = args_get_boolean_by_key(self->args, NUM_ARGS, "-low-power", false); + self->settings.exclude_metadata = args_get_boolean_by_key(self->args, NUM_ARGS, "-exclude-metadata", false); - self->audio_bitrate = args_get_i64_by_key(self->args, NUM_ARGS, "-ab", 0); - self->audio_bitrate *= 1000LL; + self->settings.audio_bitrate = args_get_i64_by_key(self->args, NUM_ARGS, "-ab", 0); + self->settings.audio_bitrate *= 1000LL; - self->keyint = args_get_double_by_key(self->args, NUM_ARGS, "-keyint", 2.0); + self->settings.keyint = args_get_double_by_key(self->args, NUM_ARGS, "-keyint", 2.0); if(overclock) { - fprintf(stderr, "gsr info: the overclock option (-oc) is deprecated and no longer has any effect as it's no longer needed (on GPUs that are 12 years old or younger)\n"); + gsr_log(GSR_LOG_LEVEL_INFO, "the overclock option (-oc) is deprecated and no longer has any effect as it's no longer needed (on GPUs that are 12 years old or younger)"); } - if(self->audio_codec == GSR_AUDIO_CODEC_FLAC) { - fprintf(stderr, "gsr warning: flac audio codec is temporary disabled, using opus audio codec instead\n"); - self->audio_codec = GSR_AUDIO_CODEC_OPUS; + if(self->settings.audio_codec == GSR_AUDIO_CODEC_FLAC) { + gsr_log(GSR_LOG_LEVEL_WARNING, "flac audio codec is temporary disabled, using opus audio codec instead"); + self->settings.audio_codec = GSR_AUDIO_CODEC_OPUS; } - self->portal_session_token_filepath = args_get_value_by_key(self->args, NUM_ARGS, "-portal-session-token-filepath"); - if(self->portal_session_token_filepath) { - int len = strlen(self->portal_session_token_filepath); - if(len > 0 && self->portal_session_token_filepath[len - 1] == '/') { - fprintf(stderr, "gsr error: -portal-session-token-filepath should be a path to a file but it ends with a /: %s\n", self->portal_session_token_filepath); + self->settings.portal_session_token_filepath = args_get_value_by_key(self->args, NUM_ARGS, "-portal-session-token-filepath"); + if(self->settings.portal_session_token_filepath) { + int len = strlen(self->settings.portal_session_token_filepath); + if(len > 0 && self->settings.portal_session_token_filepath[len - 1] == '/') { + gsr_log(GSR_LOG_LEVEL_ERROR, "-portal-session-token-filepath should be a path to a file but it ends with a /: %s", self->settings.portal_session_token_filepath); return false; } } - self->recording_saved_script = args_get_value_by_key(self->args, NUM_ARGS, "-sc"); - if(self->recording_saved_script) { + self->settings.recording_saved_script = args_get_value_by_key(self->args, NUM_ARGS, "-sc"); + if(self->settings.recording_saved_script) { struct stat buf; - if(stat(self->recording_saved_script, &buf) == -1 || !S_ISREG(buf.st_mode)) { - fprintf(stderr, "gsr error: Script \"%s\" either doesn't exist or it's not a file\n", self->recording_saved_script); + if(stat(self->settings.recording_saved_script, &buf) == -1 || !S_ISREG(buf.st_mode)) { + gsr_log(GSR_LOG_LEVEL_ERROR, "Script \"%s\" either doesn't exist or it's not a file", self->settings.recording_saved_script); usage(); return false; } if(!(buf.st_mode & S_IXUSR)) { - fprintf(stderr, "gsr error: Script \"%s\" is not executable\n", self->recording_saved_script); + gsr_log(GSR_LOG_LEVEL_ERROR, "Script \"%s\" is not executable", self->settings.recording_saved_script); usage(); return false; } } const char *quality_str = args_get_value_by_key(self->args, NUM_ARGS, "-q"); - self->video_quality = GSR_VIDEO_QUALITY_VERY_HIGH; - self->video_bitrate = 0; + self->settings.video_quality = GSR_VIDEO_QUALITY_VERY_HIGH; + self->settings.video_bitrate = 0; - if(self->bitrate_mode == GSR_BITRATE_MODE_CBR) { + if(self->settings.bitrate_mode == GSR_BITRATE_MODE_CBR) { if(!quality_str) { - fprintf(stderr, "gsr error: option '-q' is required when using '-bm cbr' option\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "option '-q' is required when using '-bm cbr' option"); usage(); return false; } - if(sscanf(quality_str, "%" PRIi64, &self->video_bitrate) != 1) { - fprintf(stderr, "gsr error: -q argument \"%s\" is not an integer value. When using '-bm cbr' option '-q' is expected to be an integer value\n", quality_str); + if(sscanf(quality_str, "%" PRIi64, &self->settings.video_bitrate) != 1) { + gsr_log(GSR_LOG_LEVEL_ERROR, "-q argument \"%s\" is not an integer value. When using '-bm cbr' option '-q' is expected to be an integer value", quality_str); usage(); return false; } - if(self->video_bitrate < 0) { - fprintf(stderr, "gsr error: -q is expected to be 0 or larger, got %" PRIi64 "\n", self->video_bitrate); + if(self->settings.video_bitrate < 0) { + gsr_log(GSR_LOG_LEVEL_ERROR, "-q is expected to be 0 or larger, got %" PRIi64, self->settings.video_bitrate); usage(); return false; } - self->video_bitrate *= 1000LL; + self->settings.video_bitrate *= 1000LL; } else { if(!quality_str) quality_str = "very_high"; if(strcmp(quality_str, "medium") == 0) { - self->video_quality = GSR_VIDEO_QUALITY_MEDIUM; + self->settings.video_quality = GSR_VIDEO_QUALITY_MEDIUM; } else if(strcmp(quality_str, "high") == 0) { - self->video_quality = GSR_VIDEO_QUALITY_HIGH; + self->settings.video_quality = GSR_VIDEO_QUALITY_HIGH; } else if(strcmp(quality_str, "very_high") == 0) { - self->video_quality = GSR_VIDEO_QUALITY_VERY_HIGH; + self->settings.video_quality = GSR_VIDEO_QUALITY_VERY_HIGH; } else if(strcmp(quality_str, "ultra") == 0) { - self->video_quality = GSR_VIDEO_QUALITY_ULTRA; + self->settings.video_quality = GSR_VIDEO_QUALITY_ULTRA; } else { - fprintf(stderr, "gsr error: -q should either be 'medium', 'high', 'very_high' or 'ultra', got: '%s'\n", quality_str); + gsr_log(GSR_LOG_LEVEL_ERROR, "-q should either be 'medium', 'high', 'very_high' or 'ultra', got: '%s'", quality_str); usage(); return false; } } - self->output_resolution = (vec2i){0, 0}; + self->settings.output_resolution = (vec2i){0, 0}; const char *output_resolution_str = args_get_value_by_key(self->args, NUM_ARGS, "-s"); if(output_resolution_str) { - if(sscanf(output_resolution_str, "%dx%d", &self->output_resolution.x, &self->output_resolution.y) != 2) { - fprintf(stderr, "gsr error: invalid value for option -s '%s', expected a value in format WxH\n", output_resolution_str); + if(sscanf(output_resolution_str, "%dx%d", &self->settings.output_resolution.x, &self->settings.output_resolution.y) != 2) { + gsr_log(GSR_LOG_LEVEL_ERROR, "invalid value for option -s '%s', expected a value in format WxH", output_resolution_str); usage(); return false; } - if(self->output_resolution.x < 0 || self->output_resolution.y < 0) { - fprintf(stderr, "gsr error: invalid value for option -s '%s', expected width and height to be greater or equal to 0\n", output_resolution_str); + if(self->settings.output_resolution.x < 0 || self->settings.output_resolution.y < 0) { + gsr_log(GSR_LOG_LEVEL_ERROR, "invalid value for option -s '%s', expected width and height to be greater or equal to 0", output_resolution_str); usage(); return false; } } - self->region_size = (vec2i){0, 0}; - self->region_position = (vec2i){0, 0}; + self->settings.region_size = (vec2i){0, 0}; + self->settings.region_position = (vec2i){0, 0}; const char *region_str = args_get_value_by_key(self->args, NUM_ARGS, "-region"); if(region_str) { - if(sscanf(region_str, "%dx%d+%d+%d", &self->region_size.x, &self->region_size.y, &self->region_position.x, &self->region_position.y) != 4) { - fprintf(stderr, "gsr error: invalid value for option -region '%s', expected a value in format WxH+X+Y\n", region_str); + if(sscanf(region_str, "%dx%d+%d+%d", &self->settings.region_size.x, &self->settings.region_size.y, &self->settings.region_position.x, &self->settings.region_position.y) != 4) { + gsr_log(GSR_LOG_LEVEL_ERROR, "invalid value for option -region '%s', expected a value in format WxH+X+Y", region_str); usage(); return false; } - if(self->region_size.x < 0 || self->region_size.y < 0) { - fprintf(stderr, "gsr error: invalid value for option -region '%s', expected width and height to be greater or equal to 0\n", region_str); + if(self->settings.region_size.x < 0 || self->settings.region_size.y < 0) { + gsr_log(GSR_LOG_LEVEL_ERROR, "invalid value for option -region '%s', expected width and height to be greater or equal to 0", region_str); usage(); return false; } } - self->fps = args_get_i64_by_key(self->args, NUM_ARGS, "-f", 60); - self->replay_buffer_size_secs = args_get_i64_by_key(self->args, NUM_ARGS, "-r", -1); - if(self->replay_buffer_size_secs != -1) - self->replay_buffer_size_secs += (int64_t)(self->keyint + 0.5); // Add a few seconds to account of lost packets because of non-keyframe packets skipped + self->settings.fps = args_get_i64_by_key(self->args, NUM_ARGS, "-f", 60); + self->settings.replay_buffer_size_secs = args_get_i64_by_key(self->args, NUM_ARGS, "-r", -1); + if(self->settings.replay_buffer_size_secs != -1) + self->settings.replay_buffer_size_secs += (int64_t)(self->settings.keyint + 0.5); // Add a few seconds to account of lost packets because of non-keyframe packets skipped - self->container_format = args_get_value_by_key(self->args, NUM_ARGS, "-c"); - if(self->container_format && strcmp(self->container_format, "mkv") == 0) - self->container_format = "matroska"; + self->settings.container_format = args_get_value_by_key(self->args, NUM_ARGS, "-c"); + if(self->settings.container_format && strcmp(self->settings.container_format, "mkv") == 0) + self->settings.container_format = "matroska"; - self->is_replaying = self->replay_buffer_size_secs != -1; - self->is_livestream = false; - self->filename = args_get_value_by_key(self->args, NUM_ARGS, "-o"); - if(self->filename) { - self->is_livestream = is_livestream_path(self->filename); - if(self->is_livestream) { - if(self->is_replaying) { - fprintf(stderr, "gsr error: replay mode is not applicable to live streaming\n"); + self->settings.is_replaying = self->settings.replay_buffer_size_secs != -1; + self->settings.is_livestream = false; + self->settings.filename = args_get_value_by_key(self->args, NUM_ARGS, "-o"); + if(self->settings.filename) { + self->settings.is_livestream = is_livestream_path(self->settings.filename); + if(self->settings.is_livestream) { + if(self->settings.is_replaying) { + gsr_log(GSR_LOG_LEVEL_ERROR, "replay mode is not applicable to live streaming"); return false; } } else { - if(!self->is_replaying) { + if(!self->settings.is_replaying) { char directory_buf[PATH_MAX]; - snprintf(directory_buf, sizeof(directory_buf), "%s", self->filename); + snprintf(directory_buf, sizeof(directory_buf), "%s", self->settings.filename); char *directory = dirname(directory_buf); if(strcmp(directory, ".") != 0 && strcmp(directory, "/") != 0) { if(create_directory_recursive(directory) != 0) { - fprintf(stderr, "gsr error: failed to create directory for output file: %s\n", self->filename); + gsr_log(GSR_LOG_LEVEL_ERROR, "failed to create directory for output file: %s", self->settings.filename); return false; } } } else { - if(!self->container_format) { - fprintf(stderr, "gsr error: option -c is required when using option -r\n"); + if(!self->settings.container_format) { + gsr_log(GSR_LOG_LEVEL_ERROR, "option -c is required when using option -r"); usage(); return false; } struct stat buf; - if(stat(self->filename, &buf) != -1 && !S_ISDIR(buf.st_mode)) { - fprintf(stderr, "gsr error: File \"%s\" exists but it's not a directory\n", self->filename); + if(stat(self->settings.filename, &buf) != -1 && !S_ISDIR(buf.st_mode)) { + gsr_log(GSR_LOG_LEVEL_ERROR, "File \"%s\" exists but it's not a directory", self->settings.filename); usage(); return false; } } } } else { - if(!self->is_replaying) { - self->filename = "/dev/stdout"; + if(!self->settings.is_replaying) { + self->settings.filename = "/dev/stdout"; } else { - fprintf(stderr, "gsr error: Option -o is required when using option -r\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "Option -o is required when using option -r"); usage(); return false; } - if(!self->container_format) { - fprintf(stderr, "gsr error: option -c is required when not using option -o\n"); + if(!self->settings.container_format) { + gsr_log(GSR_LOG_LEVEL_ERROR, "option -c is required when not using option -o"); usage(); return false; } } - self->is_output_piped = file_is_pipe_or_char_device(self->filename); - self->low_latency_recording = self->is_livestream || self->is_output_piped; - if(self->write_first_frame_ts && (self->is_livestream || self->is_output_piped)) { - fprintf(stderr, "gsr warning: -write-first-frame-ts is ignored for livestreaming or when output is piped\n"); - self->write_first_frame_ts = false; + self->settings.is_output_piped = file_is_pipe_or_char_device(self->settings.filename); + self->settings.low_latency_recording = self->settings.is_livestream || self->settings.is_output_piped; + if(self->settings.write_first_frame_ts && (self->settings.is_livestream || self->settings.is_output_piped)) { + gsr_log(GSR_LOG_LEVEL_WARNING, "-write-first-frame-ts is ignored for livestreaming or when output is piped"); + self->settings.write_first_frame_ts = false; } - self->replay_recording_directory = args_get_value_by_key(self->args, NUM_ARGS, "-ro"); + self->settings.replay_recording_directory = args_get_value_by_key(self->args, NUM_ARGS, "-ro"); - if(self->is_livestream && self->recording_saved_script) { - fprintf(stderr, "gsr warning: live stream detected, -sc script is ignored\n"); - self->recording_saved_script = NULL; + if(self->settings.is_livestream && self->settings.recording_saved_script) { + gsr_log(GSR_LOG_LEVEL_WARNING, "live stream detected, -sc script is ignored"); + self->settings.recording_saved_script = NULL; } - self->ffmpeg_opts = args_get_value_by_key(self->args, NUM_ARGS, "-ffmpeg-opts"); - self->ffmpeg_video_opts = args_get_value_by_key(self->args, NUM_ARGS, "-ffmpeg-video-opts"); - self->ffmpeg_audio_opts = args_get_value_by_key(self->args, NUM_ARGS, "-ffmpeg-audio-opts"); + self->settings.ffmpeg_opts = args_get_value_by_key(self->args, NUM_ARGS, "-ffmpeg-opts"); + self->settings.ffmpeg_video_opts = args_get_value_by_key(self->args, NUM_ARGS, "-ffmpeg-video-opts"); + self->settings.ffmpeg_audio_opts = args_get_value_by_key(self->args, NUM_ARGS, "-ffmpeg-audio-opts"); return true; } -bool args_parser_parse(args_parser *self, int argc, char **argv, const args_handlers *arg_handlers, void *userdata) { +args_parse_result args_parser_parse(args_parser *self, int argc, char **argv, const args_handlers *arg_handlers, void *userdata, int *command_exit_code) { assert(arg_handlers); memset(self, 0, sizeof(*self)); if(argc <= 1) { usage_full(); - return false; + return ARGS_PARSE_RESULT_ERROR; } if(argc == 2 && (strcmp(argv[1], "-h") == 0 || strcmp(argv[1], "--help") == 0)) { usage_full(); - return false; + return ARGS_PARSE_RESULT_ERROR; } if(argc == 2 && strcmp(argv[1], "--info") == 0) { - arg_handlers->info(userdata); - return true; + *command_exit_code = arg_handlers->info(userdata); + return ARGS_PARSE_RESULT_COMMAND_HANDLED; } if(argc == 2 && strcmp(argv[1], "--list-audio-devices") == 0) { - arg_handlers->list_audio_devices(userdata); - return true; + *command_exit_code = arg_handlers->list_audio_devices(userdata); + return ARGS_PARSE_RESULT_COMMAND_HANDLED; } if(argc == 2 && strcmp(argv[1], "--list-application-audio") == 0) { - arg_handlers->list_application_audio(userdata); - return true; + *command_exit_code = arg_handlers->list_application_audio(userdata); + return ARGS_PARSE_RESULT_COMMAND_HANDLED; } if(argc == 2 && strcmp(argv[1], "--list-v4l2-devices") == 0) { - arg_handlers->list_v4l2_devices(userdata); - return true; + *command_exit_code = arg_handlers->list_v4l2_devices(userdata); + return ARGS_PARSE_RESULT_COMMAND_HANDLED; } if(strcmp(argv[1], "--list-capture-options") == 0) { if(argc == 2) { - arg_handlers->list_capture_options(NULL, userdata); - return true; + *command_exit_code = arg_handlers->list_capture_options(NULL, userdata); + return ARGS_PARSE_RESULT_COMMAND_HANDLED; } else if(argc == 3 || argc == 4) { const char *card_path = argv[2]; - arg_handlers->list_capture_options(card_path, userdata); - return true; + *command_exit_code = arg_handlers->list_capture_options(card_path, userdata); + return ARGS_PARSE_RESULT_COMMAND_HANDLED; } else { - fprintf(stderr, "gsr error: expected --list-capture-options to be called with either no extra arguments or 1 extra argument (card path)\n"); - return false; + gsr_log(GSR_LOG_LEVEL_ERROR, "expected --list-capture-options to be called with either no extra arguments or 1 extra argument (card path)"); + return ARGS_PARSE_RESULT_ERROR; } } if(strcmp(argv[1], "--list-monitors") == 0) { - arg_handlers->list_monitors(userdata); - return true; + *command_exit_code = arg_handlers->list_monitors(userdata); + return ARGS_PARSE_RESULT_COMMAND_HANDLED; } if(argc == 2 && strcmp(argv[1], "--version") == 0) { - arg_handlers->version(userdata); - return true; + *command_exit_code = arg_handlers->version(userdata); + return ARGS_PARSE_RESULT_COMMAND_HANDLED; } int arg_index = 0; @@ -569,27 +571,28 @@ bool args_parser_parse(args_parser *self, int argc, char **argv, const args_hand self->args[arg_index++] = (Arg){ .key = "-write-first-frame-ts", .optional = true, .list = false, .type = ARG_TYPE_BOOLEAN }; self->args[arg_index++] = (Arg){ .key = "-low-power", .optional = true, .list = false, .type = ARG_TYPE_BOOLEAN }; self->args[arg_index++] = (Arg){ .key = "-exclude-metadata", .optional = true, .list = false, .type = ARG_TYPE_BOOLEAN }; + self->args[arg_index++] = (Arg){ .key = "-ipc", .optional = true, .list = false, .type = ARG_TYPE_STRING }; assert(arg_index == NUM_ARGS); for(int i = 1; i < argc; i += 2) { const char *arg_name = argv[i]; Arg *arg = args_get_by_key(self->args, NUM_ARGS, arg_name); if(!arg) { - fprintf(stderr, "gsr error: invalid argument '%s'\n", arg_name); + gsr_log(GSR_LOG_LEVEL_ERROR, "invalid argument '%s'", arg_name); usage(); - return false; + return ARGS_PARSE_RESULT_ERROR; } if(arg->num_values > 0 && !arg->list) { - fprintf(stderr, "gsr error: expected argument '%s' to only be specified once\n", arg_name); + gsr_log(GSR_LOG_LEVEL_ERROR, "expected argument '%s' to only be specified once", arg_name); usage(); - return false; + return ARGS_PARSE_RESULT_ERROR; } if(i + 1 >= argc) { - fprintf(stderr, "gsr error: missing value for argument '%s'\n", arg_name); + gsr_log(GSR_LOG_LEVEL_ERROR, "missing value for argument '%s'", arg_name); usage(); - return false; + return ARGS_PARSE_RESULT_ERROR; } const char *arg_value = argv[i + 1]; @@ -603,80 +606,80 @@ bool args_parser_parse(args_parser *self, int argc, char **argv, const args_hand } else if(strcmp(arg_value, "no") == 0) { arg->typed_value.boolean = false; } else { - fprintf(stderr, "gsr error: %s should either be 'yes' or 'no', got: '%s'\n", arg_name, arg_value); + gsr_log(GSR_LOG_LEVEL_ERROR, "%s should either be 'yes' or 'no', got: '%s'", arg_name, arg_value); usage(); - return false; + return ARGS_PARSE_RESULT_ERROR; } break; } case ARG_TYPE_ENUM: { if(!arg_get_enum_value_by_name(arg, arg_value, &arg->typed_value.enum_value)) { - fprintf(stderr, "gsr error: %s should either be ", arg_name); - arg_print_expected_enum_names(arg); - fprintf(stderr, ", got: '%s'\n", arg_value); + char expected_enum_names[512]; + arg_get_expected_enum_names(arg, expected_enum_names, sizeof(expected_enum_names)); + gsr_log(GSR_LOG_LEVEL_ERROR, "%s should either be %s, got: '%s'", arg_name, expected_enum_names, arg_value); usage(); - return false; + return ARGS_PARSE_RESULT_ERROR; } break; } case ARG_TYPE_I64: { if(sscanf(arg_value, "%" PRIi64, &arg->typed_value.i64_value) != 1) { - fprintf(stderr, "gsr error: %s argument \"%s\" is not an integer\n", arg_name, arg_value); + gsr_log(GSR_LOG_LEVEL_ERROR, "%s argument \"%s\" is not an integer", arg_name, arg_value); usage(); - return false; + return ARGS_PARSE_RESULT_ERROR; } if(arg->typed_value.i64_value < arg->integer_value_min) { - fprintf(stderr, "gsr error: %s argument is expected to be larger than %" PRIi64 ", got %" PRIi64 "\n", arg_name, arg->integer_value_min, arg->typed_value.i64_value); + gsr_log(GSR_LOG_LEVEL_ERROR, "%s argument is expected to be larger than %" PRIi64 ", got %" PRIi64, arg_name, arg->integer_value_min, arg->typed_value.i64_value); usage(); - return false; + return ARGS_PARSE_RESULT_ERROR; } if(arg->typed_value.i64_value > arg->integer_value_max) { - fprintf(stderr, "gsr error: %s argument is expected to be less than %" PRIi64 ", got %" PRIi64 "\n", arg_name, arg->integer_value_max, arg->typed_value.i64_value); + gsr_log(GSR_LOG_LEVEL_ERROR, "%s argument is expected to be less than %" PRIi64 ", got %" PRIi64, arg_name, arg->integer_value_max, arg->typed_value.i64_value); usage(); - return false; + return ARGS_PARSE_RESULT_ERROR; } break; } case ARG_TYPE_DOUBLE: { if(sscanf(arg_value, "%lf", &arg->typed_value.d_value) != 1) { - fprintf(stderr, "gsr error: %s argument \"%s\" is not an floating-point number\n", arg_name, arg_value); + gsr_log(GSR_LOG_LEVEL_ERROR, "%s argument \"%s\" is not an floating-point number", arg_name, arg_value); usage(); - return false; + return ARGS_PARSE_RESULT_ERROR; } if(arg->typed_value.d_value < arg->integer_value_min) { - fprintf(stderr, "gsr error: %s argument is expected to be larger than %" PRIi64 ", got %lf\n", arg_name, arg->integer_value_min, arg->typed_value.d_value); + gsr_log(GSR_LOG_LEVEL_ERROR, "%s argument is expected to be larger than %" PRIi64 ", got %lf", arg_name, arg->integer_value_min, arg->typed_value.d_value); usage(); - return false; + return ARGS_PARSE_RESULT_ERROR; } if(arg->typed_value.d_value > arg->integer_value_max) { - fprintf(stderr, "gsr error: %s argument is expected to be less than %" PRIi64 ", got %lf\n", arg_name, arg->integer_value_max, arg->typed_value.d_value); + gsr_log(GSR_LOG_LEVEL_ERROR, "%s argument is expected to be less than %" PRIi64 ", got %lf", arg_name, arg->integer_value_max, arg->typed_value.d_value); usage(); - return false; + return ARGS_PARSE_RESULT_ERROR; } break; } } if(!arg_append_value(arg, arg_value)) { - fprintf(stderr, "gsr error: failed to append argument, out of memory\n"); - return false; + gsr_log(GSR_LOG_LEVEL_ERROR, "failed to append argument, out of memory"); + return ARGS_PARSE_RESULT_ERROR; } } for(int i = 0; i < NUM_ARGS; ++i) { const Arg *arg = &self->args[i]; if(!arg->optional && arg->num_values == 0) { - fprintf(stderr, "gsr error: missing argument '%s'\n", arg->key); + gsr_log(GSR_LOG_LEVEL_ERROR, "missing argument '%s'", arg->key); usage(); - return false; + return ARGS_PARSE_RESULT_ERROR; } } - return args_parser_set_values(self); + return args_parser_set_values(self) ? ARGS_PARSE_RESULT_OK : ARGS_PARSE_RESULT_ERROR; } void args_parser_deinit(args_parser *self) { @@ -688,34 +691,33 @@ void args_parser_deinit(args_parser *self) { bool args_parser_validate_with_gl_info(args_parser *self, gsr_egl *egl) { const bool wayland = gsr_window_get_display_server(egl->window) == GSR_DISPLAY_SERVER_WAYLAND; - if(self->bitrate_mode == (gsr_bitrate_mode)GSR_BITRATE_MODE_AUTO) { + if(self->settings.bitrate_mode == (gsr_bitrate_mode)GSR_BITRATE_MODE_AUTO) { // QP is broken on steam deck, see https://github.com/ValveSoftware/SteamOS/issues/1609 - self->bitrate_mode = egl->gpu_info.is_steam_deck ? GSR_BITRATE_MODE_VBR : GSR_BITRATE_MODE_QP; + self->settings.bitrate_mode = egl->gpu_info.is_steam_deck ? GSR_BITRATE_MODE_VBR : GSR_BITRATE_MODE_QP; } - if(egl->gpu_info.is_steam_deck && self->bitrate_mode == GSR_BITRATE_MODE_QP) { - fprintf(stderr, "gsr warning: qp bitrate mode is not supported on Steam Deck because of Steam Deck driver bugs. Using vbr instead\n"); - self->bitrate_mode = GSR_BITRATE_MODE_VBR; + if(egl->gpu_info.is_steam_deck && self->settings.bitrate_mode == GSR_BITRATE_MODE_QP) { + gsr_log(GSR_LOG_LEVEL_WARNING, "qp bitrate mode is not supported on Steam Deck because of Steam Deck driver bugs. Using vbr instead"); + self->settings.bitrate_mode = GSR_BITRATE_MODE_VBR; } - if(self->video_encoder == GSR_VIDEO_ENCODER_HW_CPU && self->bitrate_mode == GSR_BITRATE_MODE_VBR) { - fprintf(stderr, "gsr warning: bitrate mode has been forcefully set to qp because software encoding option doesn't support vbr option\n"); - self->bitrate_mode = GSR_BITRATE_MODE_QP; + if(self->settings.video_encoder == GSR_VIDEO_ENCODER_HW_CPU && self->settings.bitrate_mode == GSR_BITRATE_MODE_VBR) { + gsr_log(GSR_LOG_LEVEL_WARNING, "bitrate mode has been forcefully set to qp because software encoding option doesn't support vbr option"); + self->settings.bitrate_mode = GSR_BITRATE_MODE_QP; } if(egl->gpu_info.is_steam_deck) { - fprintf(stderr, "gsr warning: steam deck has multiple driver issues. One of them has been reported here: https://github.com/ValveSoftware/SteamOS/issues/1609\n" - "If you have issues with GPU Screen Recorder on steam deck that you don't have on a desktop computer then report the issue to Valve and/or AMD.\n"); + gsr_log(GSR_LOG_LEVEL_WARNING, "steam deck has multiple driver issues. One of them has been reported here: https://github.com/ValveSoftware/SteamOS/issues/1609\nIf you have issues with GPU Screen Recorder on steam deck that you don't have on a desktop computer then report the issue to Valve and/or AMD."); } - self->very_old_gpu = false; + self->settings.very_old_gpu = false; if(egl->gpu_info.vendor == GSR_GPU_VENDOR_NVIDIA && egl->gpu_info.gpu_version != 0 && egl->gpu_info.gpu_version < 900) { - fprintf(stderr, "gsr info: your gpu appears to be very old (older than maxwell architecture). Switching to lower preset\n"); - self->very_old_gpu = true; + gsr_log(GSR_LOG_LEVEL_INFO, "your gpu appears to be very old (older than maxwell architecture). Switching to lower preset"); + self->settings.very_old_gpu = true; } - if(video_codec_is_hdr(self->video_codec) && !wayland) { - fprintf(stderr, "gsr error: hdr video codec option %s is not available on X11\n", video_codec_to_string(self->video_codec)); + if(video_codec_is_hdr(self->settings.video_codec) && !wayland) { + gsr_log(GSR_LOG_LEVEL_ERROR, "hdr video codec option %s is not available on X11", video_codec_to_string(self->settings.video_codec)); usage(); return false; } diff --git a/src/capture/kms.c b/src/capture/kms.c index 0f7c37c..2285fb6 100644 --- a/src/capture/kms.c +++ b/src/capture/kms.c @@ -1,13 +1,12 @@ #include "../../include/capture/kms.h" +#include "../../include/log.h" #include "../../include/utils.h" #include "../../include/color_conversion.h" -#include "../../include/cursor.h" #include "../../include/kde_night_light.h" #include "../../include/window/window.h" #include <stdlib.h> #include <string.h> -#include <stdio.h> #include <unistd.h> #include <fcntl.h> @@ -159,7 +158,7 @@ static void monitor_callback(const gsr_monitor *monitor, void *userdata) { } if(monitor_callback_userdata->monitor_id->num_connector_ids == MAX_CONNECTOR_IDS) - fprintf(stderr, "gsr warning: reached max connector ids\n"); + gsr_log(GSR_LOG_LEVEL_WARNING, "reached max connector ids"); } static vec2i rotate_capture_size_if_rotated(gsr_capture_kms *self, vec2i capture_size, gsr_monitor_rotation rotation) { @@ -191,7 +190,7 @@ static int gsr_capture_kms_start(gsr_capture *cap, gsr_capture_metadata *capture for_each_active_monitor_output(self->params.egl->window, self->params.egl->card_path, connection_type, monitor_callback, &monitor_callback_userdata); if(!get_monitor_by_name(self->params.egl, connection_type, self->params.display_to_capture, &monitor)) { - fprintf(stderr, "gsr error: gsr_capture_kms_start: failed to find monitor by name \"%s\"\n", self->params.display_to_capture); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_capture_kms_start: failed to find monitor by name \"%s\"", self->params.display_to_capture); gsr_capture_kms_stop(self); return -1; } @@ -454,7 +453,7 @@ static void gsr_capture_kms_update_hdr_color_transforms(gsr_capture_kms *self, g const bool night_light = self->params.kde_night_light && gsr_kde_night_light_get_inverse_matrix(self->params.kde_night_light, night_light_compensation, night_light_container_primaries, night_light_matrix); if(night_light && !self->night_light_message_shown) { self->night_light_message_shown = true; - fprintf(stderr, "gsr info: gsr_capture_kms_update_hdr_color_transforms: night light is active, removing the night light tint from the capture\n"); + gsr_log(GSR_LOG_LEVEL_INFO, "gsr_capture_kms_update_hdr_color_transforms: night light is active, removing the night light tint from the capture"); } /* The compositor applies the night light tint at scanout with the crtc gamma lut when it offloads its color transforms to the crtc @@ -468,24 +467,21 @@ static void gsr_capture_kms_update_hdr_color_transforms(gsr_capture_kms *self, g luminance_scale = SDR_WHITE_LUMINANCE_REFERENCE / sdr_white_luminance; if(luminance_scale != 1.0f && !self->hdr_luminance_message_shown) { self->hdr_luminance_message_shown = true; - fprintf(stderr, "gsr info: gsr_capture_kms_update_hdr_color_transforms: scaling the luminance of the hdr video from sdr white at %d nits to sdr white at %d nits (hdr peak luminance: %d nits)\n", - (int)sdr_white_luminance, (int)SDR_WHITE_LUMINANCE_REFERENCE, (int)(hdr_peak_luminance * luminance_scale)); + gsr_log(GSR_LOG_LEVEL_INFO, "gsr_capture_kms_update_hdr_color_transforms: scaling the luminance of the hdr video from sdr white at %d nits to sdr white at %d nits (hdr peak luminance: %d nits)", (int)sdr_white_luminance, (int)SDR_WHITE_LUMINANCE_REFERENCE, (int)(hdr_peak_luminance * luminance_scale)); } } const bool convert_sdr_to_hdr = self->params.hdr && !plane_is_hdr; if(convert_sdr_to_hdr && !self->hdr_luminance_message_shown) { self->hdr_luminance_message_shown = true; - fprintf(stderr, "gsr info: gsr_capture_kms_update_hdr_color_transforms: the monitor is in sdr mode, converting the captured sdr image to hdr (BT.709 to BT.2020 with sdr white at %d nits)\n", - (int)SDR_WHITE_LUMINANCE_REFERENCE); + gsr_log(GSR_LOG_LEVEL_INFO, "gsr_capture_kms_update_hdr_color_transforms: the monitor is in sdr mode, converting the captured sdr image to hdr (BT.709 to BT.2020 with sdr white at %d nits)", (int)SDR_WHITE_LUMINANCE_REFERENCE); } const bool tone_map_hdr_to_sdr = !self->params.hdr && plane_is_hdr; gsr_color_conversion_set_hdr_to_sdr_tone_mapping(color_conversion, tone_map_hdr_to_sdr, hdr_peak_luminance, sdr_white_luminance); if(tone_map_hdr_to_sdr && !self->tone_mapping_message_shown) { self->tone_mapping_message_shown = true; - fprintf(stderr, "gsr info: gsr_capture_kms_update_hdr_color_transforms: the monitor is in hdr mode, tone mapping the hdr content to sdr (sdr white luminance: %d nits, hdr peak luminance: %d nits). Record with -k hevc_hdr or -k av1_hdr video codec option to record hdr instead\n", - (int)sdr_white_luminance, (int)hdr_peak_luminance); + gsr_log(GSR_LOG_LEVEL_INFO, "gsr_capture_kms_update_hdr_color_transforms: the monitor is in hdr mode, tone mapping the hdr content to sdr (sdr white luminance: %d nits, hdr peak luminance: %d nits). Record with -k hevc_hdr or -k av1_hdr video codec option to record hdr instead", (int)sdr_white_luminance, (int)hdr_peak_luminance); } if(convert_sdr_to_hdr) { @@ -615,7 +611,7 @@ static EGLImage gsr_capture_kms_create_egl_image_with_fallback(gsr_capture_kms * } else { image = gsr_capture_kms_create_egl_image(self, drm_fd, fds, offsets, pitches, modifiers, true); if(!image) { - fprintf(stderr, "gsr error: gsr_capture_kms_create_egl_image_with_fallback: failed to create egl image with modifiers, trying without modifiers\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_capture_kms_create_egl_image_with_fallback: failed to create egl image with modifiers, trying without modifiers"); self->no_modifiers_fallback = true; image = gsr_capture_kms_create_egl_image(self, drm_fd, fds, offsets, pitches, modifiers, false); } @@ -638,7 +634,7 @@ static void gsr_capture_kms_bind_image_to_input_texture_with_fallback(gsr_captur gsr_capture_kms_bind_image_to_texture(self, image, self->external_input_texture_id, true); } else { if(!gsr_capture_kms_bind_image_to_texture(self, image, self->input_texture_id, false)) { - fprintf(stderr, "gsr error: gsr_capture_kms_capture: failed to bind image to texture, trying with external texture\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_capture_kms_capture: failed to bind image to texture, trying with external texture"); self->external_texture_fallback = true; gsr_capture_kms_bind_image_to_texture(self, image, self->external_input_texture_id, true); } @@ -824,7 +820,7 @@ static void gsr_capture_kms_update_connector_ids(gsr_capture_kms *self) { gsr_monitor monitor; if(!get_monitor_by_name(self->params.egl, connection_type, self->params.display_to_capture, &monitor)) { - fprintf(stderr, "gsr error: gsr_capture_kms_update_connector_ids: failed to find monitor by name \"%s\"\n", self->params.display_to_capture); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_capture_kms_update_connector_ids: failed to find monitor by name \"%s\"", self->params.display_to_capture); return; } @@ -851,7 +847,7 @@ static void gsr_capture_kms_pre_capture(gsr_capture *cap, gsr_capture_metadata * static bool error_shown = false; if(!error_shown) { error_shown = true; - fprintf(stderr, "gsr error: gsr_capture_kms_pre_capture: no drm found, capture will fail\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_capture_kms_pre_capture: no drm found, capture will fail"); } return; } @@ -1105,7 +1101,7 @@ static void gsr_capture_kms_destroy(gsr_capture *cap) { gsr_capture* gsr_capture_kms_create(const gsr_capture_kms_params *params) { if(!params) { - fprintf(stderr, "gsr error: gsr_capture_kms_create params is NULL\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_capture_kms_create params is NULL"); return NULL; } diff --git a/src/capture/nvfbc.c b/src/capture/nvfbc.c index 1504323..8f386d4 100644 --- a/src/capture/nvfbc.c +++ b/src/capture/nvfbc.c @@ -1,4 +1,5 @@ #include "../../include/capture/nvfbc.h" +#include "../../include/log.h" #include "../../external/NvFBC.h" #include "../../include/egl.h" #include "../../include/utils.h" @@ -8,7 +9,6 @@ #include <dlfcn.h> #include <stdlib.h> #include <string.h> -#include <stdio.h> #include <math.h> #include <assert.h> @@ -74,13 +74,13 @@ static bool gsr_capture_nvfbc_load_library(gsr_capture *cap) { dlerror(); /* clear */ void *lib = dlopen("libnvidia-fbc.so.1", RTLD_LAZY); if(!lib) { - fprintf(stderr, "gsr error: failed to load libnvidia-fbc.so.1, error: %s\n", dlerror()); + gsr_log(GSR_LOG_LEVEL_ERROR, "failed to load libnvidia-fbc.so.1, error: %s", dlerror()); return false; } set_func_ptr((void**)&self->nv_fbc_create_instance, dlsym(lib, "NvFBCCreateInstance")); if(!self->nv_fbc_create_instance) { - fprintf(stderr, "gsr error: unable to resolve symbol 'NvFBCCreateInstance'\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "unable to resolve symbol 'NvFBCCreateInstance'"); dlclose(lib); return false; } @@ -89,7 +89,7 @@ static bool gsr_capture_nvfbc_load_library(gsr_capture *cap) { self->nv_fbc_function_list.dwVersion = NVFBC_VERSION; NVFBCSTATUS status = self->nv_fbc_create_instance(&self->nv_fbc_function_list); if(status != NVFBC_SUCCESS) { - fprintf(stderr, "gsr error: failed to create NvFBC instance (status: %d)\n", status); + gsr_log(GSR_LOG_LEVEL_ERROR, "failed to create NvFBC instance (status: %d)", status); dlclose(lib); return false; } @@ -143,7 +143,7 @@ static int gsr_capture_nvfbc_setup_handle(gsr_capture_nvfbc *self) { status = self->nv_fbc_function_list.nvFBCCreateHandle(&self->nv_fbc_handle, &create_params); if(status != NVFBC_SUCCESS) { - fprintf(stderr, "gsr error: gsr_capture_nvfbc_start failed: %s\n", self->nv_fbc_function_list.nvFBCGetLastErrorStr(self->nv_fbc_handle)); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_capture_nvfbc_start failed: %s", self->nv_fbc_function_list.nvFBCGetLastErrorStr(self->nv_fbc_handle)); goto error_cleanup; } } @@ -155,12 +155,12 @@ static int gsr_capture_nvfbc_setup_handle(gsr_capture_nvfbc *self) { status = self->nv_fbc_function_list.nvFBCGetStatus(self->nv_fbc_handle, &status_params); if(status != NVFBC_SUCCESS) { - fprintf(stderr, "gsr error: gsr_capture_nvfbc_start failed: %s\n", self->nv_fbc_function_list.nvFBCGetLastErrorStr(self->nv_fbc_handle)); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_capture_nvfbc_start failed: %s", self->nv_fbc_function_list.nvFBCGetLastErrorStr(self->nv_fbc_handle)); goto error_cleanup; } if(status_params.bCanCreateNow == NVFBC_FALSE) { - fprintf(stderr, "gsr error: gsr_capture_nvfbc_start failed: it's not possible to create a capture session on this system\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_capture_nvfbc_start failed: it's not possible to create a capture session on this system"); goto error_cleanup; } @@ -172,18 +172,18 @@ static int gsr_capture_nvfbc_setup_handle(gsr_capture_nvfbc *self) { self->tracking_type = strcmp(self->params.display_to_capture, "screen") == 0 ? NVFBC_TRACKING_SCREEN : NVFBC_TRACKING_OUTPUT; if(self->tracking_type == NVFBC_TRACKING_OUTPUT) { if(!status_params.bXRandRAvailable) { - fprintf(stderr, "gsr error: gsr_capture_nvfbc_start failed: the xrandr extension is not available\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_capture_nvfbc_start failed: the xrandr extension is not available"); goto error_cleanup; } if(status_params.bInModeset) { - fprintf(stderr, "gsr error: gsr_capture_nvfbc_start failed: the x server is in modeset, unable to record\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_capture_nvfbc_start failed: the x server is in modeset, unable to record"); goto error_cleanup; } self->output_id = get_output_id_from_display_name(status_params.outputs, status_params.dwOutputNum, self->params.display_to_capture, &self->tracking_width, &self->tracking_height); if(self->output_id == 0) { - fprintf(stderr, "gsr error: gsr_capture_nvfbc_start failed: display '%s' not found\n", self->params.display_to_capture); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_capture_nvfbc_start failed: display '%s' not found", self->params.display_to_capture); goto error_cleanup; } } @@ -215,7 +215,7 @@ static int gsr_capture_nvfbc_setup_session(gsr_capture_nvfbc *self) { NVFBCSTATUS status = self->nv_fbc_function_list.nvFBCCreateCaptureSession(self->nv_fbc_handle, &create_capture_params); if(status != NVFBC_SUCCESS) { - fprintf(stderr, "gsr error: gsr_capture_nvfbc_start failed: %s\n", self->nv_fbc_function_list.nvFBCGetLastErrorStr(self->nv_fbc_handle)); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_capture_nvfbc_start failed: %s", self->nv_fbc_function_list.nvFBCGetLastErrorStr(self->nv_fbc_handle)); return -1; } self->capture_session_created = true; @@ -226,7 +226,7 @@ static int gsr_capture_nvfbc_setup_session(gsr_capture_nvfbc *self) { status = self->nv_fbc_function_list.nvFBCToGLSetUp(self->nv_fbc_handle, &self->setup_params); if(status != NVFBC_SUCCESS) { - fprintf(stderr, "gsr error: gsr_capture_nvfbc_start failed: %s\n", self->nv_fbc_function_list.nvFBCGetLastErrorStr(self->nv_fbc_handle)); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_capture_nvfbc_start failed: %s", self->nv_fbc_function_list.nvFBCGetLastErrorStr(self->nv_fbc_handle)); gsr_capture_nvfbc_destroy_session(self); return -1; } @@ -256,12 +256,12 @@ static int gsr_capture_nvfbc_start(gsr_capture *cap, gsr_capture_metadata *captu int driver_major_version = 0; int driver_minor_version = 0; if(self->params.direct_capture && get_nvidia_driver_version(&driver_major_version, &driver_minor_version)) { - fprintf(stderr, "gsr info: detected nvidia version: %d.%d\n", driver_major_version, driver_minor_version); + gsr_log(GSR_LOG_LEVEL_INFO, "detected nvidia version: %d.%d", driver_major_version, driver_minor_version); // TODO: if(version_at_least(driver_major_version, driver_minor_version, 515, 57) && version_less_than(driver_major_version, driver_minor_version, 520, 56)) { self->params.direct_capture = false; - fprintf(stderr, "gsr warning: \"screen-direct\" has temporary been disabled as it causes stuttering with driver versions >= 515.57 and < 520.56. Please update your driver if possible. Capturing \"screen\" instead.\n"); + gsr_log(GSR_LOG_LEVEL_WARNING, "\"screen-direct\" has temporary been disabled as it causes stuttering with driver versions >= 515.57 and < 520.56. Please update your driver if possible. Capturing \"screen\" instead."); } // TODO: @@ -271,7 +271,7 @@ static int gsr_capture_nvfbc_start(gsr_capture *cap, gsr_capture_metadata *captu if(version_at_least(driver_major_version, driver_minor_version, 515, 57)) self->supports_direct_cursor = true; else - fprintf(stderr, "gsr info: capturing \"screen-direct\" but driver version appears to be less than 515.57. Disabling capture of cursor. Please update your driver if you want to capture your cursor or record \"screen\" instead.\n"); + gsr_log(GSR_LOG_LEVEL_INFO, "capturing \"screen-direct\" but driver version appears to be less than 515.57. Disabling capture of cursor. Please update your driver if you want to capture your cursor or record \"screen\" instead."); } */ } @@ -349,16 +349,16 @@ static int gsr_capture_nvfbc_capture(gsr_capture *cap, gsr_capture_metadata *cap gsr_capture_nvfbc_destroy_session_and_handle(self); if(gsr_capture_nvfbc_setup_handle(self) != 0) { - fprintf(stderr, "gsr error: gsr_capture_nvfbc_capture failed to recreate nvfbc handle, trying again in %f second(s)\n", nvfbc_recreate_retry_time_seconds); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_capture_nvfbc_capture failed to recreate nvfbc handle, trying again in %f second(s)", nvfbc_recreate_retry_time_seconds); return -1; } if(gsr_capture_nvfbc_setup_session(self) != 0) { - fprintf(stderr, "gsr error: gsr_capture_nvfbc_capture failed to recreate nvfbc session, trying again in %f second(s)\n", nvfbc_recreate_retry_time_seconds); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_capture_nvfbc_capture failed to recreate nvfbc session, trying again in %f second(s)", nvfbc_recreate_retry_time_seconds); return -1; } - fprintf(stderr, "gsr info: gsr_capture_nvfbc_capture: recreated nvfbc session after modeset recovery\n"); + gsr_log(GSR_LOG_LEVEL_INFO, "gsr_capture_nvfbc_capture: recreated nvfbc session after modeset recovery"); self->nvfbc_needs_recreate = false; } else { return 0; @@ -385,7 +385,7 @@ static int gsr_capture_nvfbc_capture(gsr_capture *cap, gsr_capture_metadata *cap NVFBCSTATUS status = self->nv_fbc_function_list.nvFBCToGLGrabFrame(self->nv_fbc_handle, &grab_params); if(status != NVFBC_SUCCESS) { - fprintf(stderr, "gsr error: gsr_capture_nvfbc_capture failed: %s (%d), recreating session after %f second(s)\n", self->nv_fbc_function_list.nvFBCGetLastErrorStr(self->nv_fbc_handle), status, nvfbc_recreate_retry_time_seconds); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_capture_nvfbc_capture failed: %s (%d), recreating session after %f second(s)", self->nv_fbc_function_list.nvFBCGetLastErrorStr(self->nv_fbc_handle), status, nvfbc_recreate_retry_time_seconds); self->nvfbc_needs_recreate = true; self->nvfbc_dead_start = clock_get_monotonic_seconds(); return 0; @@ -414,12 +414,12 @@ static void gsr_capture_nvfbc_destroy(gsr_capture *cap) { gsr_capture* gsr_capture_nvfbc_create(const gsr_capture_nvfbc_params *params) { if(!params) { - fprintf(stderr, "gsr error: gsr_capture_nvfbc_create params is NULL\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_capture_nvfbc_create params is NULL"); return NULL; } if(!params->display_to_capture) { - fprintf(stderr, "gsr error: gsr_capture_nvfbc_create params.display_to_capture is NULL\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_capture_nvfbc_create params.display_to_capture is NULL"); return NULL; } diff --git a/src/capture/portal.c b/src/capture/portal.c index fd1caec..622cf4a 100644 --- a/src/capture/portal.c +++ b/src/capture/portal.c @@ -1,4 +1,5 @@ #include "../../include/capture/portal.h" +#include "../../include/log.h" #include "../../include/color_conversion.h" #include "../../include/egl.h" #include "../../include/utils.h" @@ -111,7 +112,7 @@ static bool create_directory_to_file(const char *filepath) { snprintf(dir, sizeof(dir), "%.*s", (int)(split - filepath), filepath); if(create_directory_recursive(dir) != 0) { - fprintf(stderr, "gsr warning: gsr_capture_portal_save_restore_token: failed to create directory (%s) for restore token\n", dir); + gsr_log(GSR_LOG_LEVEL_WARNING, "gsr_capture_portal_save_restore_token: failed to create directory (%s) for restore token", dir); return false; } return true; @@ -130,18 +131,18 @@ static void gsr_capture_portal_save_restore_token(const char *restore_token, con FILE *f = fopen(restore_token_path, "wb"); if(!f) { - fprintf(stderr, "gsr warning: gsr_capture_portal_save_restore_token: failed to create restore token file (%s)\n", restore_token_path); + gsr_log(GSR_LOG_LEVEL_WARNING, "gsr_capture_portal_save_restore_token: failed to create restore token file (%s)", restore_token_path); return; } const int restore_token_len = strlen(restore_token); if((long)fwrite(restore_token, 1, restore_token_len, f) != restore_token_len) { - fprintf(stderr, "gsr warning: gsr_capture_portal_save_restore_token: failed to write restore token to file (%s)\n", restore_token_path); + gsr_log(GSR_LOG_LEVEL_WARNING, "gsr_capture_portal_save_restore_token: failed to write restore token to file (%s)", restore_token_path); fclose(f); return; } - fprintf(stderr, "gsr info: gsr_capture_portal_save_restore_token: saved restore token to cache (%s)\n", restore_token); + gsr_log(GSR_LOG_LEVEL_INFO, "gsr_capture_portal_save_restore_token: saved restore token to cache (%s)", restore_token); fclose(f); } @@ -158,7 +159,7 @@ static void gsr_capture_portal_get_restore_token_from_cache(char *buffer, size_t FILE *f = fopen(restore_token_path, "rb"); if(!f) { - fprintf(stderr, "gsr info: gsr_capture_portal_get_restore_token_from_cache: no restore token found in cache or failed to load (%s)\n", restore_token_path); + gsr_log(GSR_LOG_LEVEL_INFO, "gsr_capture_portal_get_restore_token_from_cache: no restore token found in cache or failed to load (%s)", restore_token_path); return; } @@ -168,7 +169,7 @@ static void gsr_capture_portal_get_restore_token_from_cache(char *buffer, size_t if(file_size > 0 && file_size < 1024 && file_size < (long)buffer_size && (long)fread(buffer, 1, file_size, f) != file_size) { buffer[0] = '\0'; - fprintf(stderr, "gsr warning: gsr_capture_portal_get_restore_token_from_cache: failed to read restore token (%s)\n", restore_token_path); + gsr_log(GSR_LOG_LEVEL_WARNING, "gsr_capture_portal_get_restore_token_from_cache: failed to read restore token (%s)", restore_token_path); fclose(f); return; } @@ -176,7 +177,7 @@ static void gsr_capture_portal_get_restore_token_from_cache(char *buffer, size_t if(file_size > 0 && file_size < (long)buffer_size) buffer[file_size] = '\0'; - fprintf(stderr, "gsr info: gsr_capture_portal_get_restore_token_from_cache: read cached restore token (%s)\n", buffer); + gsr_log(GSR_LOG_LEVEL_INFO, "gsr_capture_portal_get_restore_token_from_cache: read cached restore token (%s)", buffer); fclose(f); } @@ -193,24 +194,24 @@ static int gsr_capture_portal_setup_dbus(gsr_capture_portal *self, int *pipewire if(!gsr_dbus_init(&self->dbus, restore_token)) return -1; - fprintf(stderr, "gsr info: gsr_capture_portal_setup_dbus: CreateSession\n"); + gsr_log(GSR_LOG_LEVEL_INFO, "gsr_capture_portal_setup_dbus: CreateSession"); response_status = gsr_dbus_screencast_create_session(&self->dbus, &self->session_handle); if(response_status != 0) { - fprintf(stderr, "gsr error: gsr_capture_portal_setup_dbus: CreateSession failed\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_capture_portal_setup_dbus: CreateSession failed"); return response_status; } - fprintf(stderr, "gsr info: gsr_capture_portal_setup_dbus: SelectSources\n"); + gsr_log(GSR_LOG_LEVEL_INFO, "gsr_capture_portal_setup_dbus: SelectSources"); response_status = gsr_dbus_screencast_select_sources(&self->dbus, self->session_handle, GSR_PORTAL_CAPTURE_TYPE_ALL, self->params.record_cursor ? GSR_PORTAL_CURSOR_MODE_METADATA : GSR_PORTAL_CURSOR_MODE_HIDDEN); if(response_status != 0) { - fprintf(stderr, "gsr error: gsr_capture_portal_setup_dbus: SelectSources failed\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_capture_portal_setup_dbus: SelectSources failed"); return response_status; } - fprintf(stderr, "gsr info: gsr_capture_portal_setup_dbus: Start\n"); + gsr_log(GSR_LOG_LEVEL_INFO, "gsr_capture_portal_setup_dbus: Start"); response_status = gsr_dbus_screencast_start(&self->dbus, self->session_handle, pipewire_node); if(response_status != 0) { - fprintf(stderr, "gsr error: gsr_capture_portal_setup_dbus: Start failed\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_capture_portal_setup_dbus: Start failed"); return response_status; } @@ -218,31 +219,31 @@ static int gsr_capture_portal_setup_dbus(gsr_capture_portal *self, int *pipewire if(screencast_restore_token) gsr_capture_portal_save_restore_token(screencast_restore_token, self->params.portal_session_token_filepath); - fprintf(stderr, "gsr info: gsr_capture_portal_setup_dbus: OpenPipeWireRemote\n"); + gsr_log(GSR_LOG_LEVEL_INFO, "gsr_capture_portal_setup_dbus: OpenPipeWireRemote"); if(!gsr_dbus_screencast_open_pipewire_remote(&self->dbus, self->session_handle, pipewire_fd)) { - fprintf(stderr, "gsr error: gsr_capture_portal_setup_dbus: OpenPipeWireRemote failed\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_capture_portal_setup_dbus: OpenPipeWireRemote failed"); return -1; } - fprintf(stderr, "gsr info: gsr_capture_portal_setup_dbus: desktop portal setup finished\n"); + gsr_log(GSR_LOG_LEVEL_INFO, "gsr_capture_portal_setup_dbus: desktop portal setup finished"); return 0; } static bool gsr_capture_portal_get_frame_dimensions(gsr_capture_portal *self) { - fprintf(stderr, "gsr info: gsr_capture_portal_start: waiting for pipewire negotiation\n"); + gsr_log(GSR_LOG_LEVEL_INFO, "gsr_capture_portal_start: waiting for pipewire negotiation"); const double start_time = clock_get_monotonic_seconds(); while(clock_get_monotonic_seconds() - start_time < 5.0) { if(gsr_pipewire_video_map_texture(&self->pipewire, self->texture_map, &self->pipewire_data)) { self->capture_size.x = self->pipewire_data.region.width; self->capture_size.y = self->pipewire_data.region.height; - fprintf(stderr, "gsr info: gsr_capture_portal_start: pipewire negotiation finished\n"); + gsr_log(GSR_LOG_LEVEL_INFO, "gsr_capture_portal_start: pipewire negotiation finished"); return true; } usleep(30 * 1000); /* 30 milliseconds */ } - fprintf(stderr, "gsr info: gsr_capture_portal_start: timed out waiting for pipewire negotiation (5 seconds)\n"); + gsr_log(GSR_LOG_LEVEL_INFO, "gsr_capture_portal_start: timed out waiting for pipewire negotiation (5 seconds)"); return false; } @@ -259,24 +260,24 @@ static int gsr_capture_portal_setup(gsr_capture_portal *self, int fps) { // 2: The user interaction was ended in some other way // Response status value 2 happens usually if there was some kind of error in the desktop portal on the system if(response_status == 2) { - fprintf(stderr, "gsr error: gsr_capture_portal_setup: desktop portal capture failed. Either you Wayland compositor doesn't support desktop portal capture or it's incorrectly setup on your system\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_capture_portal_setup: desktop portal capture failed. Either you Wayland compositor doesn't support desktop portal capture or it's incorrectly setup on your system"); return 50; } else if(response_status == 1) { - fprintf(stderr, "gsr error: gsr_capture_portal_setup: desktop portal capture failed. It seems like desktop portal capture was canceled by the user.\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_capture_portal_setup: desktop portal capture failed. It seems like desktop portal capture was canceled by the user."); return PORTAL_CAPTURE_CANCELED_BY_USER_EXIT_CODE; } else { return -1; } } - fprintf(stderr, "gsr info: gsr_capture_portal_setup: setting up pipewire\n"); + gsr_log(GSR_LOG_LEVEL_INFO, "gsr_capture_portal_setup: setting up pipewire"); /* TODO: support hdr when pipewire supports it */ /* gsr_pipewire closes the pipewire fd, even on failure */ if(!gsr_pipewire_video_init(&self->pipewire, pipewire_fd, pipewire_node, fps, self->params.record_cursor, self->params.egl)) { - fprintf(stderr, "gsr error: gsr_capture_portal_setup: failed to setup pipewire with fd: %d, node: %" PRIu32 "\n", pipewire_fd, pipewire_node); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_capture_portal_setup: failed to setup pipewire with fd: %d, node: %" PRIu32, pipewire_fd, pipewire_node); return -1; } - fprintf(stderr, "gsr info: gsr_capture_portal_setup: pipewire setup finished\n"); + gsr_log(GSR_LOG_LEVEL_INFO, "gsr_capture_portal_setup: pipewire setup finished"); if(!gsr_capture_portal_get_frame_dimensions(self)) return -1; @@ -329,7 +330,7 @@ static void gsr_capture_portal_pre_capture(gsr_capture *cap, gsr_capture_metadat return; if(gsr_pipewire_video_should_restart(&self->pipewire)) { - fprintf(stderr, "gsr info: gsr_capture_portal_pre_capture: pipewire capture was paused, trying to start capture again\n"); + gsr_log(GSR_LOG_LEVEL_INFO, "gsr_capture_portal_pre_capture: pipewire capture was paused, trying to start capture again"); gsr_capture_portal_stop(self); const int result = gsr_capture_portal_setup(self, capture_metadata->fps); if(result != 0) { @@ -452,7 +453,7 @@ static void gsr_capture_portal_destroy(gsr_capture *cap) { gsr_capture* gsr_capture_portal_create(const gsr_capture_portal_params *params) { if(!params) { - fprintf(stderr, "gsr error: gsr_capture_portal_create params is NULL\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_capture_portal_create params is NULL"); return NULL; } diff --git a/src/capture/v4l2.c b/src/capture/v4l2.c index a4e81e4..22f0f59 100644 --- a/src/capture/v4l2.c +++ b/src/capture/v4l2.c @@ -1,4 +1,5 @@ #include "../../include/capture/v4l2.h" +#include "../../include/log.h" #include "../../include/color_conversion.h" #include "../../include/egl.h" #include "../../include/utils.h" @@ -367,12 +368,12 @@ static void gsr_capture_v4l2_update_params(int fd) { .type = V4L2_BUF_TYPE_VIDEO_CAPTURE, }; if(xioctl(fd, VIDIOC_G_PARM, &streamparm) == -1) { - fprintf(stderr, "gsr error: gsr_capture_v4l2_set_framerate: VIDIOC_G_PARM failed, error: %s\n", strerror(errno)); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_capture_v4l2_set_framerate: VIDIOC_G_PARM failed, error: %s", strerror(errno)); return; } if(xioctl(fd, VIDIOC_S_PARM, &streamparm) == -1) { - fprintf(stderr, "gsr error: gsr_capture_v4l2_set_framerate: VIDIOC_S_PARM failed, error: %s\n", strerror(errno)); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_capture_v4l2_set_framerate: VIDIOC_S_PARM failed, error: %s", strerror(errno)); return; } } @@ -382,19 +383,19 @@ static void gsr_capture_v4l2_set_framerate(int fd, gsr_capture_v4l2_framerate fr .type = V4L2_BUF_TYPE_VIDEO_CAPTURE, }; if(xioctl(fd, VIDIOC_G_PARM, &streamparm) == -1) { - fprintf(stderr, "gsr error: gsr_capture_v4l2_set_framerate: VIDIOC_G_PARM failed, error: %s\n", strerror(errno)); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_capture_v4l2_set_framerate: VIDIOC_G_PARM failed, error: %s", strerror(errno)); return; } streamparm.parm.capture.timeperframe.denominator = framerate.denominator; streamparm.parm.capture.timeperframe.numerator = framerate.numerator; if(xioctl(fd, VIDIOC_S_PARM, &streamparm) == -1) { - fprintf(stderr, "gsr error: gsr_capture_v4l2_set_framerate: VIDIOC_S_PARM failed, error: %s\n", strerror(errno)); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_capture_v4l2_set_framerate: VIDIOC_S_PARM failed, error: %s", strerror(errno)); return; } if(streamparm.parm.capture.timeperframe.denominator == 0 || streamparm.parm.capture.timeperframe.numerator == 0) { - fprintf(stderr, "gsr error: gsr_capture_v4l2_set_framerate: VIDIOC_S_PARM failed, error: invalid framerate: %u/%u\n", framerate.denominator, framerate.numerator);; + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_capture_v4l2_set_framerate: VIDIOC_S_PARM failed, error: invalid framerate: %u/%u", framerate.denominator, framerate.numerator);; return; } } @@ -403,21 +404,21 @@ static bool gsr_capture_v4l2_validate_pixfmt(const gsr_capture_v4l2 *self, const switch(self->params.pixfmt) { case GSR_CAPTURE_V4L2_PIXFMT_AUTO: { if(!supported_pixfmts.yuyv && !supported_pixfmts.mjpeg) { - fprintf(stderr, "gsr error: gsr_capture_v4l2_create: %s doesn't support yuyv nor mjpeg. GPU Screen Recorder supports only yuyv and mjpeg at the moment. Report this as an issue, see: https://git.dec05eba.com/?p=about\n", self->params.device_path); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_capture_v4l2_create: %s doesn't support yuyv nor mjpeg. GPU Screen Recorder supports only yuyv and mjpeg at the moment. Report this as an issue, see: https://git.dec05eba.com/?p=about", self->params.device_path); return false; } break; } case GSR_CAPTURE_V4L2_PIXFMT_YUYV: { if(!supported_pixfmts.yuyv) { - fprintf(stderr, "gsr error: gsr_capture_v4l2_create: %s doesn't support yuyv. Try recording with pixfmt=mjpeg or pixfmt=auto instead\n", self->params.device_path); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_capture_v4l2_create: %s doesn't support yuyv. Try recording with pixfmt=mjpeg or pixfmt=auto instead", self->params.device_path); return false; } break; } case GSR_CAPTURE_V4L2_PIXFMT_MJPEG: { if(!supported_pixfmts.mjpeg) { - fprintf(stderr, "gsr error: gsr_capture_v4l2_create: %s doesn't support mjpeg. Try recording with pixfmt=yuyv or pixfmt=auto instead\n", self->params.device_path); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_capture_v4l2_create: %s doesn't support mjpeg. Try recording with pixfmt=yuyv or pixfmt=auto instead", self->params.device_path); return false; } break; @@ -432,7 +433,7 @@ static bool gsr_capture_v4l2_create_pbos(gsr_capture_v4l2 *self, int width, int self->params.egl->glGenBuffers(NUM_PBOS, self->pbos); for(int i = 0; i < NUM_PBOS; ++i) { if(self->pbos[i] == 0) { - fprintf(stderr, "gsr error: gsr_capture_v4l2_create_pbos: failed to create pixel buffer objects\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_capture_v4l2_create_pbos: failed to create pixel buffer objects"); return false; } @@ -477,7 +478,7 @@ static bool gsr_capture_v4l2_map_buffer(gsr_capture_v4l2 *self, const struct v4l }); if(!self->dma_image[i]) { - fprintf(stderr, "gsr error: gsr_capture_v4l2_map_buffer: eglCreateImage failed, error: %d\n", self->params.egl->eglGetError()); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_capture_v4l2_map_buffer: eglCreateImage failed, error: %d", self->params.egl->eglGetError()); return false; } } @@ -488,7 +489,7 @@ static bool gsr_capture_v4l2_map_buffer(gsr_capture_v4l2 *self, const struct v4l self->params.egl->glTexParameteri(GL_TEXTURE_EXTERNAL_OES, GL_TEXTURE_MAG_FILTER, GL_LINEAR); self->params.egl->glBindTexture(GL_TEXTURE_EXTERNAL_OES, 0); if(self->texture_id[i] == 0) { - fprintf(stderr, "gsr error: gsr_capture_v4l2_map_buffer: failed to create texture\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_capture_v4l2_map_buffer: failed to create texture"); return false; } } @@ -501,14 +502,14 @@ static bool gsr_capture_v4l2_map_buffer(gsr_capture_v4l2 *self, const struct v4l self->dmabuf_size[i] = fmt->fmt.pix.sizeimage; self->dmabuf_map[i] = mmap(NULL, fmt->fmt.pix.sizeimage, PROT_READ, MAP_SHARED, self->dmabuf_fd[i], 0); if(self->dmabuf_map[i] == MAP_FAILED) { - fprintf(stderr, "gsr error: gsr_capture_v4l2_map_buffer: mmap failed, error: %s\n", strerror(errno)); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_capture_v4l2_map_buffer: mmap failed, error: %s", strerror(errno)); return false; } // GL_RGBA is intentionally used here instead of GL_RGB, because the performance is much better when using glTexSubImage2D (22% cpu usage compared to 38% cpu usage) self->texture_id[i] = gl_create_texture(self->params.egl, fmt->fmt.pix.width, fmt->fmt.pix.height, GL_RGBA8, GL_RGBA, GL_LINEAR); if(self->texture_id[i] == 0) { - fprintf(stderr, "gsr error: gsr_capture_v4l2_map_buffer: failed to create texture\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_capture_v4l2_map_buffer: failed to create texture"); return false; } } @@ -534,28 +535,28 @@ static bool is_libturbojpeg_library_available(void) { static int gsr_capture_v4l2_setup(gsr_capture_v4l2 *self) { self->fd = open(self->params.device_path, O_RDWR | O_NONBLOCK); if(self->fd < 0) { - fprintf(stderr, "gsr error: gsr_capture_v4l2_create: failed to open %s, error: %s\n", self->params.device_path, strerror(errno)); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_capture_v4l2_create: failed to open %s, error: %s", self->params.device_path, strerror(errno)); return -1; } struct v4l2_capability cap = {0}; if(xioctl(self->fd, VIDIOC_QUERYCAP, &cap) == -1) { if(EINVAL == errno) { - fprintf(stderr, "gsr error: gsr_capture_v4l2_create: %s isn't a v4l2 device\n", self->params.device_path); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_capture_v4l2_create: %s isn't a v4l2 device", self->params.device_path); return -1; } else { - fprintf(stderr, "gsr error: gsr_capture_v4l2_create: VIDIOC_QUERYCAP failed, error: %s\n", strerror(errno)); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_capture_v4l2_create: VIDIOC_QUERYCAP failed, error: %s", strerror(errno)); return -1; } } if(!(cap.capabilities & V4L2_CAP_VIDEO_CAPTURE)) { - fprintf(stderr, "gsr error: gsr_capture_v4l2_create: %s isn't a video capture device\n", self->params.device_path); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_capture_v4l2_create: %s isn't a video capture device", self->params.device_path); return -1; } if(!(cap.capabilities & V4L2_CAP_STREAMING)) { - fprintf(stderr, "gsr error: gsr_capture_v4l2_create: %s doesn't support streaming i/o\n", self->params.device_path); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_capture_v4l2_create: %s doesn't support streaming i/o", self->params.device_path); return -1; } @@ -563,7 +564,7 @@ static int gsr_capture_v4l2_setup(gsr_capture_v4l2 *self) { const bool has_libturbojpeg_lib = is_libturbojpeg_library_available(); if(!has_libturbojpeg_lib && self->params.pixfmt == GSR_CAPTURE_V4L2_PIXFMT_AUTO) { - fprintf(stderr, "gsr warning: gsr_capture_v4l2_create: libturbojpeg.so.0 isn't available on the system, yuyv camera capture will be used\n"); + gsr_log(GSR_LOG_LEVEL_WARNING, "gsr_capture_v4l2_create: libturbojpeg.so.0 isn't available on the system, yuyv camera capture will be used"); self->params.pixfmt = GSR_CAPTURE_V4L2_PIXFMT_YUYV; } @@ -576,12 +577,11 @@ static int gsr_capture_v4l2_setup(gsr_capture_v4l2 *self) { gsr_capture_v4l2_supported_setup best_supported_setup = {0}; if(!gsr_capture_v4l2_get_best_matching_setup(supported_setups, num_supported_setups, self->params.pixfmt, self->params.camera_fps, self->params.camera_resolution, &best_supported_setup)) { - fprintf(stderr, "gsr error: gsr_capture_v4l2_create: %s doesn't report any frame resolutions and framerates\n", self->params.device_path); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_capture_v4l2_create: %s doesn't report any frame resolutions and framerates", self->params.device_path); return -1; } - fprintf(stderr, "gsr info: gsr_capture_v4l2_create: capturing %s at %ux%u@%dhz, pixfmt: %s\n", - self->params.device_path, + gsr_log(GSR_LOG_LEVEL_INFO, "gsr_capture_v4l2_create: capturing %s at %ux%u@%dhz, pixfmt: %s", self->params.device_path, best_supported_setup.resolution.width, best_supported_setup.resolution.height, gsr_capture_v4l2_framerate_to_number(best_supported_setup.framerate), @@ -594,7 +594,7 @@ static int gsr_capture_v4l2_setup(gsr_capture_v4l2 *self) { dlerror(); /* clear */ self->libturbojpeg_lib = dlopen("libturbojpeg.so.0", RTLD_LAZY); if(!self->libturbojpeg_lib) { - fprintf(stderr, "gsr error: gsr_capture_v4l2_create: failed to load libturbojpeg.so.0 which is required for camera mjpeg capture, error: %s\n", dlerror()); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_capture_v4l2_create: failed to load libturbojpeg.so.0 which is required for camera mjpeg capture, error: %s", dlerror()); return -1; } @@ -605,13 +605,13 @@ static int gsr_capture_v4l2_setup(gsr_capture_v4l2 *self) { self->tjGetErrorStr2 = (FUNC_tjGetErrorStr2)dlsym(self->libturbojpeg_lib, "tjGetErrorStr2"); if(!self->tjInitDecompress || !self->tjDestroy || !self->tjDecompressHeader2 || !self->tjDecompress2 || !self->tjGetErrorStr2) { - fprintf(stderr, "gsr error: gsr_capture_v4l2_create: libturbojpeg.so.0 is missing functions. The libturbojpeg version installed on your system might be outdated\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_capture_v4l2_create: libturbojpeg.so.0 is missing functions. The libturbojpeg version installed on your system might be outdated"); return -1; } self->jpeg_decompressor = self->tjInitDecompress(); if(!self->jpeg_decompressor) { - fprintf(stderr, "gsr error: gsr_capture_v4l2_create: failed to create jpeg decompressor\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_capture_v4l2_create: failed to create jpeg decompressor"); return -1; } } @@ -624,12 +624,12 @@ static int gsr_capture_v4l2_setup(gsr_capture_v4l2 *self) { .fmt.pix.height = best_supported_setup.resolution.height, }; if(xioctl(self->fd, VIDIOC_S_FMT, &fmt) == -1) { - fprintf(stderr, "gsr error: gsr_capture_v4l2_create: VIDIOC_S_FMT failed, error: %s\n", strerror(errno)); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_capture_v4l2_create: VIDIOC_S_FMT failed, error: %s", strerror(errno)); return -1; } if(fmt.fmt.pix.pixelformat != v4l2_pixfmt) { - fprintf(stderr, "gsr error: gsr_capture_v4l2_create: pixel format isn't as requested (got pixel format: %u, requested: %u), error: %s\n", fmt.fmt.pix.pixelformat, v4l2_pixfmt, strerror(errno)); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_capture_v4l2_create: pixel format isn't as requested (got pixel format: %u, requested: %u), error: %s", fmt.fmt.pix.pixelformat, v4l2_pixfmt, strerror(errno)); return -1; } @@ -644,7 +644,7 @@ static int gsr_capture_v4l2_setup(gsr_capture_v4l2 *self) { .count = NUM_BUFFERS }; if(xioctl(self->fd, VIDIOC_REQBUFS, &reqbuf) == -1) { - fprintf(stderr, "gsr error: gsr_capture_v4l2_create: VIDIOC_REQBUFS failed, error: %s\n", strerror(errno)); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_capture_v4l2_create: VIDIOC_REQBUFS failed, error: %s", strerror(errno)); return -1; } @@ -655,7 +655,7 @@ static int gsr_capture_v4l2_setup(gsr_capture_v4l2 *self) { .flags = O_RDONLY }; if(xioctl(self->fd, VIDIOC_EXPBUF, &expbuf) == -1) { - fprintf(stderr, "gsr error: gsr_capture_v4l2_create: VIDIOC_EXPBUF failed, error: %s\n", strerror(errno)); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_capture_v4l2_create: VIDIOC_EXPBUF failed, error: %s", strerror(errno)); return -1; } self->dmabuf_fd[i] = expbuf.fd; @@ -674,11 +674,11 @@ static int gsr_capture_v4l2_setup(gsr_capture_v4l2 *self) { } if(xioctl(self->fd, VIDIOC_STREAMON, &(enum v4l2_buf_type){V4L2_BUF_TYPE_VIDEO_CAPTURE})) { - fprintf(stderr, "gsr error: gsr_capture_v4l2_create: VIDIOC_STREAMON failed, error: %s\n", strerror(errno)); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_capture_v4l2_create: VIDIOC_STREAMON failed, error: %s", strerror(errno)); return -1; } - fprintf(stderr, "gsr info: gsr_capture_v4l2_create: waiting for camera %s to be ready\n", self->params.device_path); + gsr_log(GSR_LOG_LEVEL_INFO, "gsr_capture_v4l2_create: waiting for camera %s to be ready", self->params.device_path); return 0; } @@ -707,7 +707,7 @@ static void gsr_capture_v4l2_tick(gsr_capture *cap) { if(!self->got_first_frame && !self->should_stop) { const double timeout_sec = 5.0; if(clock_get_monotonic_seconds() - self->capture_start_time >= timeout_sec) { - fprintf(stderr, "gsr error: gsr_capture_v4l2_capture: didn't receive camera data in %f seconds\n", timeout_sec); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_capture_v4l2_capture: didn't receive camera data in %f seconds", timeout_sec); self->should_stop = true; self->stop_is_error = true; } @@ -719,12 +719,12 @@ static void gsr_capture_v4l2_decode_jpeg_to_texture(gsr_capture_v4l2 *self, cons int jpeg_width = 0; int jpeg_height = 0; if(self->tjDecompressHeader2(self->jpeg_decompressor, self->dmabuf_map[buf->index], buf->bytesused, &jpeg_width, &jpeg_height, &jpeg_subsamp) != 0) { - fprintf(stderr, "gsr error: gsr_capture_v4l2_capture: failed to decompress camera jpeg header data, error: %s\n", self->tjGetErrorStr2(self->jpeg_decompressor)); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_capture_v4l2_capture: failed to decompress camera jpeg header data, error: %s", self->tjGetErrorStr2(self->jpeg_decompressor)); return; } if(jpeg_width != self->capture_size.x || jpeg_height != self->capture_size.y) { - fprintf(stderr, "gsr error: gsr_capture_v4l2_capture: got jpeg data of incorrect dimensions. Expected %dx%d, got %dx%d\n", self->capture_size.x, self->capture_size.y, jpeg_width, jpeg_height); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_capture_v4l2_capture: got jpeg data of incorrect dimensions. Expected %dx%d, got %dx%d", self->capture_size.x, self->capture_size.y, jpeg_width, jpeg_height); return; } @@ -742,7 +742,7 @@ static void gsr_capture_v4l2_decode_jpeg_to_texture(gsr_capture_v4l2 *self, cons void *mapped_buffer = self->params.egl->glMapBufferRange(GL_PIXEL_UNPACK_BUFFER, 0, self->capture_size.x * self->capture_size.y * 4, GL_MAP_WRITE_BIT); if(mapped_buffer) { if(self->tjDecompress2(self->jpeg_decompressor, self->dmabuf_map[buf->index], buf->bytesused, mapped_buffer, jpeg_width, 0, jpeg_height, TJPF_RGBA, TJFLAG_FASTDCT) != 0) - fprintf(stderr, "gsr error: gsr_capture_v4l2_capture: failed to decompress camera jpeg data, error: %s\n", self->tjGetErrorStr2(self->jpeg_decompressor)); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_capture_v4l2_capture: failed to decompress camera jpeg data, error: %s", self->tjGetErrorStr2(self->jpeg_decompressor)); self->params.egl->glUnmapBuffer(GL_PIXEL_UNPACK_BUFFER); } @@ -764,7 +764,7 @@ static int gsr_capture_v4l2_capture(gsr_capture *cap, gsr_capture_metadata *capt if(buf.bytesused > 0 && !(buf.flags & V4L2_BUF_FLAG_ERROR)) { if(!self->got_first_frame) - fprintf(stderr, "gsr info: gsr_capture_v4l2_capture: camera %s is now ready\n", self->params.device_path); + gsr_log(GSR_LOG_LEVEL_INFO, "gsr_capture_v4l2_capture: camera %s is now ready", self->params.device_path); self->got_first_frame = true; switch(self->buffer_type) { @@ -852,7 +852,7 @@ static void gsr_capture_v4l2_destroy(gsr_capture *cap) { gsr_capture* gsr_capture_v4l2_create(const gsr_capture_v4l2_params *params) { if(!params) { - fprintf(stderr, "gsr error: gsr_capture_v4l2_create params is NULL\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_capture_v4l2_create params is NULL"); return NULL; } diff --git a/src/capture/xcomposite.c b/src/capture/xcomposite.c index c1bef4f..ace04b9 100644 --- a/src/capture/xcomposite.c +++ b/src/capture/xcomposite.c @@ -1,14 +1,11 @@ #include "../../include/capture/xcomposite.h" +#include "../../include/log.h" #include "../../include/window_texture.h" #include "../../include/utils.h" -#include "../../include/cursor.h" #include "../../include/color_conversion.h" #include "../../include/window/window.h" #include <stdlib.h> -#include <stdio.h> -#include <string.h> -#include <assert.h> #include <X11/Xlib.h> @@ -63,7 +60,7 @@ static int gsr_capture_xcomposite_start(gsr_capture *cap, gsr_capture_metadata * if(self->params.follow_focused) { self->net_active_window_atom = XInternAtom(self->display, "_NET_ACTIVE_WINDOW", False); if(!self->net_active_window_atom) { - fprintf(stderr, "gsr error: gsr_capture_xcomposite_start failed: failed to get _NET_ACTIVE_WINDOW atom\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_capture_xcomposite_start failed: failed to get _NET_ACTIVE_WINDOW atom"); return -1; } self->window = get_focused_window(self->display, self->net_active_window_atom); @@ -75,7 +72,7 @@ static int gsr_capture_xcomposite_start(gsr_capture *cap, gsr_capture_metadata * XWindowAttributes attr; if(!XGetWindowAttributes(self->display, self->window, &attr) && !self->params.follow_focused) { - fprintf(stderr, "gsr error: gsr_capture_xcomposite_start failed: invalid window id: %lu\n", self->window); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_capture_xcomposite_start failed: invalid window id: %lu", self->window); return -1; } @@ -89,7 +86,7 @@ static int gsr_capture_xcomposite_start(gsr_capture *cap, gsr_capture_metadata * XSelectInput(self->display, self->window, StructureNotifyMask | ExposureMask); if(window_texture_init(&self->window_texture, self->display, self->window, self->params.egl) != 0 && !self->params.follow_focused) { - fprintf(stderr, "gsr error: gsr_capture_xcomposite_start: failed to get window texture for window %ld\n", (long)self->window); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_capture_xcomposite_start: failed to get window texture for window %ld", (long)self->window); return -1; } @@ -129,7 +126,7 @@ static void gsr_capture_xcomposite_tick(gsr_capture *cap) { attr.width = 0; attr.height = 0; if(!XGetWindowAttributes(self->display, self->window, &attr)) - fprintf(stderr, "gsr error: gsr_capture_xcomposite_tick failed: invalid window id: %lu\n", self->window); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_capture_xcomposite_tick failed: invalid window id: %lu", self->window); self->window_pos.x = attr.x; self->window_pos.y = attr.y; @@ -153,7 +150,7 @@ static void gsr_capture_xcomposite_tick(gsr_capture *cap) { self->window_resized = false; if(window_texture_on_resize(&self->window_texture) != 0) { - fprintf(stderr, "gsr error: gsr_capture_xcomposite_tick: window_texture_on_resize failed\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_capture_xcomposite_tick: window_texture_on_resize failed"); //self->should_stop = true; //self->stop_is_error = true; return; @@ -291,7 +288,7 @@ static void gsr_capture_xcomposite_destroy(gsr_capture *cap) { gsr_capture* gsr_capture_xcomposite_create(const gsr_capture_xcomposite_params *params) { if(!params) { - fprintf(stderr, "gsr error: gsr_capture_xcomposite_create params is NULL\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_capture_xcomposite_create params is NULL"); return NULL; } diff --git a/src/capture/ximage.c b/src/capture/ximage.c index b0cf364..d80cd0b 100644 --- a/src/capture/ximage.c +++ b/src/capture/ximage.c @@ -1,13 +1,11 @@ #include "../../include/capture/ximage.h" +#include "../../include/log.h" #include "../../include/utils.h" -#include "../../include/cursor.h" #include "../../include/color_conversion.h" #include "../../include/window/window.h" #include <stdlib.h> -#include <stdio.h> #include <string.h> -#include <assert.h> #include <X11/Xlib.h> @@ -39,7 +37,7 @@ static int gsr_capture_ximage_start(gsr_capture *cap, gsr_capture_metadata *capt self->root_window = DefaultRootWindow(self->display); if(!get_monitor_by_name(self->params.egl, GSR_CONNECTION_X11, self->params.display_to_capture, &self->monitor)) { - fprintf(stderr, "gsr error: gsr_capture_ximage_start: failed to find monitor by name \"%s\"\n", self->params.display_to_capture); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_capture_ximage_start: failed to find monitor by name \"%s\"", self->params.display_to_capture); gsr_capture_ximage_stop(self); return -1; } @@ -61,7 +59,7 @@ static int gsr_capture_ximage_start(gsr_capture *cap, gsr_capture_metadata *capt self->texture_id = gl_create_texture(self->params.egl, self->capture_size.x, self->capture_size.y, GL_RGB8, GL_RGB, GL_LINEAR); if(self->texture_id == 0) { - fprintf(stderr, "gsr error: gsr_capture_ximage_start: failed to create texture\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_capture_ximage_start: failed to create texture"); gsr_capture_ximage_stop(self); return -1; } @@ -95,14 +93,14 @@ static bool gsr_capture_ximage_upload_to_texture(gsr_capture_ximage *self, int x XImage *image = XGetImage(self->display, self->root_window, x, y, width, height, AllPlanes, ZPixmap); if(!image) { - fprintf(stderr, "gsr error: gsr_capture_ximage_upload_to_texture: XGetImage failed\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_capture_ximage_upload_to_texture: XGetImage failed"); return false; } bool success = false; uint8_t *image_data = malloc(image->width * image->height * 3); if(!image_data) { - fprintf(stderr, "gsr error: gsr_capture_ximage_upload_to_texture: failed to allocate image data\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_capture_ximage_upload_to_texture: failed to allocate image data"); goto done; } @@ -186,7 +184,7 @@ static void gsr_capture_ximage_destroy(gsr_capture *cap) { gsr_capture* gsr_capture_ximage_create(const gsr_capture_ximage_params *params) { if(!params) { - fprintf(stderr, "gsr error: gsr_capture_ximage_create params is NULL\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_capture_ximage_create params is NULL"); return NULL; } diff --git a/src/cli/commands.c b/src/cli/commands.c new file mode 100644 index 0000000..9b77f79 --- /dev/null +++ b/src/cli/commands.c @@ -0,0 +1,366 @@ +#include "../../include/cli/commands.h" +#include "../../include/recorder/windowing.h" +#include "../../include/recorder/codec_select.h" +#include "../../include/recorder/error.h" +#include "../../include/capture/v4l2.h" +#include "../../include/window/window.h" +#include "../../include/sound.h" +#include "../../include/utils.h" +#include "../../include/log.h" +#ifdef GSR_APP_AUDIO +#include "../../include/pipewire_audio.h" +#endif +#ifdef GSR_PORTAL +#include "../../include/dbus.h" +#endif + +#include <stdio.h> +#include <stdlib.h> +#include <unistd.h> +#include <sys/wait.h> + +#ifndef GSR_VERSION +#define GSR_VERSION "unknown" +#endif + +static void list_system_info(bool wayland) { + printf("display_server|%s\n", wayland ? "wayland" : "x11"); + bool supports_app_audio = false; +#ifdef GSR_APP_AUDIO + supports_app_audio = pulseaudio_server_is_pipewire(); + if(supports_app_audio) { + gsr_pipewire_audio audio; + if(gsr_pipewire_audio_init(&audio)) + gsr_pipewire_audio_deinit(&audio); + else + supports_app_audio = false; + } +#endif + printf("supports_app_audio|%s\n", supports_app_audio ? "yes" : "no"); +} + +static void list_gpu_info(gsr_egl *egl) { + switch(egl->gpu_info.vendor) { + case GSR_GPU_VENDOR_AMD: + printf("vendor|amd\n"); + break; + case GSR_GPU_VENDOR_INTEL: + printf("vendor|intel\n"); + break; + case GSR_GPU_VENDOR_NVIDIA: + printf("vendor|nvidia\n"); + break; + case GSR_GPU_VENDOR_BROADCOM: + printf("vendor|broadcom\n"); + break; + case GSR_GPU_VENDOR_APPLE: + printf("vendor|apple\n"); + break; + } + printf("card_path|%s\n", egl->card_path); +} + +static void list_supported_video_codecs(gsr_egl *egl, bool wayland) { + // Dont clean it up on purpose to increase shutdown speed + gsr_supported_video_codecs supported_video_codecs; + get_supported_video_codecs(egl, GSR_VIDEO_CODEC_H264, false, false, &supported_video_codecs); + + gsr_supported_video_codecs supported_video_codecs_vulkan; + get_supported_video_codecs(egl, GSR_VIDEO_CODEC_H264_VULKAN, false, false, &supported_video_codecs_vulkan); + + set_supported_video_codecs_ffmpeg(&supported_video_codecs, &supported_video_codecs_vulkan, egl->gpu_info.vendor); + + if(supported_video_codecs.h264.supported) + puts("h264"); + if(avcodec_find_encoder_by_name("libx264")) + puts("h264_software"); + if(supported_video_codecs.hevc.supported) + puts("hevc"); + if(supported_video_codecs.hevc_hdr.supported && wayland) + puts("hevc_hdr"); + if(supported_video_codecs.hevc_10bit.supported) + puts("hevc_10bit"); + if(supported_video_codecs.av1.supported) + puts("av1"); + if(supported_video_codecs.av1_hdr.supported && wayland) + puts("av1_hdr"); + if(supported_video_codecs.av1_10bit.supported) + puts("av1_10bit"); + if(supported_video_codecs.vp8.supported) + puts("vp8"); + if(supported_video_codecs.vp9.supported) + puts("vp9"); + if(supported_video_codecs_vulkan.h264.supported) + puts("h264_vulkan"); + if(supported_video_codecs_vulkan.hevc.supported) + puts("hevc_vulkan"); + if(supported_video_codecs_vulkan.hevc_hdr.supported && wayland) + puts("hevc_hdr_vulkan"); + if(supported_video_codecs_vulkan.hevc_10bit.supported) + puts("hevc_10bit_vulkan"); + if(supported_video_codecs_vulkan.av1.supported) + puts("av1_vulkan"); + if(supported_video_codecs_vulkan.av1_hdr.supported && wayland) + puts("av1_hdr_vulkan"); + if(supported_video_codecs_vulkan.av1_10bit.supported) + puts("av1_10bit_vulkan"); +} + +void run_recording_saved_script_async(const char *script_file, const char *video_file, const char *type) { + char script_file_full[PATH_MAX]; + script_file_full[0] = '\0'; + if(!realpath(script_file, script_file_full)) { + gsr_log(GSR_LOG_LEVEL_ERROR, "script file not found: %s", script_file); + return; + } + + const char *args[7]; + const bool inside_flatpak = getenv("FLATPAK_ID") != NULL; + + if(inside_flatpak) { + args[0] = "flatpak-spawn"; + args[1] = "--host"; + args[2] = "--"; + args[3] = script_file_full; + args[4] = video_file; + args[5] = type; + args[6] = NULL; + } else { + args[0] = script_file_full; + args[1] = video_file; + args[2] = type; + args[3] = NULL; + } + + pid_t pid = fork(); + if(pid == -1) { + perror(script_file_full); + return; + } else if(pid == 0) { // child + setsid(); + signal(SIGHUP, SIG_IGN); + + pid_t second_child = fork(); + if(second_child == 0) { // child + execvp(args[0], (char* const*)args); + perror(script_file_full); + _exit(127); + } else if(second_child != -1) { // parent + _exit(0); + } + } else { // parent + waitpid(pid, NULL, 0); + } +} + +typedef struct { + const gsr_window *window; + int num_monitors; +} capture_options_callback; + +static void output_monitor_info(const gsr_monitor *monitor, void *userdata) { + capture_options_callback *options = (capture_options_callback*)userdata; + if(gsr_window_get_display_server(options->window) == GSR_DISPLAY_SERVER_WAYLAND) { + vec2i monitor_size = monitor->size; + gsr_monitor_rotation monitor_rotation = GSR_MONITOR_ROT_0; + vec2i monitor_position = {0, 0}; + drm_monitor_get_display_server_data(options->window, monitor, &monitor_rotation, &monitor_position); + if(monitor_rotation == GSR_MONITOR_ROT_90 || monitor_rotation == GSR_MONITOR_ROT_270) + { + const int tmp = monitor_size.x; + monitor_size.x = monitor_size.y; + monitor_size.y = tmp; + } + printf("%.*s|%dx%d\n", monitor->name_len, monitor->name, monitor_size.x, monitor_size.y); + } else { + printf("%.*s|%dx%d\n", monitor->name_len, monitor->name, monitor->size.x, monitor->size.y); + } + ++options->num_monitors; +} + +static void camera_query_callback(const char *path, const gsr_capture_v4l2_supported_setup *setup, void *userdata) { + (void)userdata; + printf("%s|%ux%u@%uhz|%s\n", path, setup->resolution.width, setup->resolution.height, gsr_capture_v4l2_framerate_to_number(setup->framerate), gsr_capture_v4l2_pixfmt_to_string(setup->pixfmt)); +} + +static int list_monitors(const gsr_window *window, const char *card_path) { + capture_options_callback options; + options.window = window; + options.num_monitors = 0; + + const bool is_x11 = gsr_window_get_display_server(window) == GSR_DISPLAY_SERVER_X11; + const gsr_connection_type connection_type = is_x11 ? GSR_CONNECTION_X11 : GSR_CONNECTION_DRM; + for_each_active_monitor_output(window, card_path, connection_type, output_monitor_info, &options); + + return options.num_monitors; +} + +static void list_supported_capture_options(const gsr_window *window, const char *card_path, bool do_list_monitors) { + const bool wayland = gsr_window_get_display_server(window) == GSR_DISPLAY_SERVER_WAYLAND; + if(!wayland) { + puts("window"); + puts("focused"); + } + + int num_monitors = 0; + if(do_list_monitors) + num_monitors = list_monitors(window, card_path); + + if(num_monitors > 0) + puts("region"); + + gsr_capture_v4l2_list_devices(camera_query_callback, NULL); + +#ifdef GSR_PORTAL + // Desktop portal capture on x11 doesn't seem to be hardware accelerated + if(!wayland) + return; + + gsr_dbus dbus; + if(!gsr_dbus_init(&dbus, NULL)) + return; + + char *session_handle = NULL; + if(gsr_dbus_screencast_create_session(&dbus, &session_handle) == 0) + puts("portal"); + + gsr_dbus_deinit(&dbus); +#endif +} + +int version_command(void *userdata) { + (void)userdata; + puts(GSR_VERSION); + fflush(stdout); + return 0; +} + +int info_command(void *userdata) { + (void)userdata; + gsr_windowing windowing; + const gsr_windowing_params windowing_params = { .monitor_capture = true }; + if(gsr_windowing_init(&windowing, &windowing_params) != GSR_ERROR_OK) + return 1; + + if(gsr_windowing_load_egl(&windowing, &windowing_params) != GSR_ERROR_OK) { + gsr_windowing_deinit(&windowing); + return 22; + } + + const bool wayland = gsr_windowing_is_wayland(&windowing); + + av_log_set_level(AV_LOG_FATAL); + + puts("section=system_info"); + list_system_info(wayland); + if(windowing.egl.gpu_info.is_steam_deck) + puts("is_steam_deck|yes"); + else + puts("is_steam_deck|no"); + printf("gsr_version|%s\n", GSR_VERSION); + puts("section=gpu_info"); + list_gpu_info(&windowing.egl); + puts("section=video_codecs"); + list_supported_video_codecs(&windowing.egl, wayland); + puts("section=image_formats"); + puts("jpeg"); + puts("png"); + puts("section=capture_options"); + list_supported_capture_options(windowing.window, windowing.egl.card_path, windowing.card_path_found); + + fflush(stdout); + gsr_windowing_deinit(&windowing); + return 0; +} + +int list_audio_devices_command(void *userdata) { + (void)userdata; + gsr_audio_devices audio_devices; + get_pulseaudio_inputs(&audio_devices); + + if(audio_devices.default_output[0] != '\0') + puts("default_output|Default output"); + + if(audio_devices.default_input[0] != '\0') + puts("default_input|Default input"); + + for(size_t i = 0; i < audio_devices.num_items; ++i) { + printf("%s|%s\n", audio_devices.items[i].name, audio_devices.items[i].description); + } + + gsr_audio_devices_deinit(&audio_devices); + fflush(stdout); + return 0; +} + +static bool app_audio_query_callback(const char *app_name, void *userdata) { + (void)userdata; + puts(app_name); + return true; +} + +int list_application_audio_command(void *userdata) { + (void)userdata; +#ifdef GSR_APP_AUDIO + if(pulseaudio_server_is_pipewire()) { + gsr_pipewire_audio audio; + if(gsr_pipewire_audio_init(&audio)) { + gsr_pipewire_audio_for_each_app(&audio, app_audio_query_callback, NULL); + gsr_pipewire_audio_deinit(&audio); + } + } +#endif + + fflush(stdout); + return 0; +} + +int list_v4l2_devices(void *userdata) { + (void)userdata; + gsr_capture_v4l2_list_devices(camera_query_callback, NULL); + + fflush(stdout); + return 0; +} + +int list_capture_options_command(const char *card_path, void *userdata) { + (void)userdata; + gsr_windowing windowing; + const gsr_windowing_params windowing_params = { .monitor_capture = true }; + if(gsr_windowing_init(&windowing, &windowing_params) != GSR_ERROR_OK) + return 1; + + if(!card_path && gsr_windowing_load_egl(&windowing, &windowing_params) != GSR_ERROR_OK) { + gsr_windowing_deinit(&windowing); + return 22; + } + + if(card_path) + list_supported_capture_options(windowing.window, card_path, true); + else + list_supported_capture_options(windowing.window, windowing.egl.card_path, windowing.card_path_found); + + fflush(stdout); + gsr_windowing_deinit(&windowing); + return 0; +} + +int list_monitors_command(void *userdata) { + (void)userdata; + gsr_windowing windowing; + const gsr_windowing_params windowing_params = { .monitor_capture = true }; + if(gsr_windowing_init(&windowing, &windowing_params) != GSR_ERROR_OK) + return 1; + + if(gsr_windowing_load_egl(&windowing, &windowing_params) != GSR_ERROR_OK) { + gsr_windowing_deinit(&windowing); + return 22; + } + + if(windowing.card_path_found) + list_monitors(windowing.window, windowing.egl.card_path); + + fflush(stdout); + gsr_windowing_deinit(&windowing); + return 0; +} diff --git a/src/cli/ipc.c b/src/cli/ipc.c new file mode 100644 index 0000000..2fb3000 --- /dev/null +++ b/src/cli/ipc.c @@ -0,0 +1,962 @@ +#include "../../include/cli/ipc.h" +#include "../../include/recorder/error.h" +#include "../../include/recorder/replay_save.h" +#include "../../include/json.h" +#include "../../include/log.h" + +#include <stdio.h> +#include <string.h> +#include <errno.h> +#include <fcntl.h> +#include <inttypes.h> +#include <limits.h> +#include <poll.h> +#include <signal.h> +#include <unistd.h> +#include <sys/socket.h> +#include <sys/stat.h> +#include <sys/un.h> + +#ifdef __linux__ +#include <sys/epoll.h> +#else +#include <sys/event.h> +#include <sys/time.h> +#endif + +#define GSR_IPC_MAX_REQUEST_NAME_SIZE 64 +#define GSR_IPC_MAX_EVENTS (2 + GSR_IPC_MAX_CLIENTS*2) +#define GSR_IPC_SHUTDOWN_SEND_TIMEOUT_MILLISECONDS 1000 +#define GSR_IPC_SOCKET_MODE 0600 + +#define GSR_IPC_WAKEUP_QUIT (1 << 0) +#define GSR_IPC_WAKEUP_COMPLETED_REQUEST (1 << 1) + +typedef struct { + int64_t id; + char name[GSR_IPC_MAX_REQUEST_NAME_SIZE]; + sj_Value data; + bool has_data; + const char *request_end; +} gsr_ipc_request; + +typedef struct { + int fd; + bool readable; + bool writable; +} gsr_ipc_event; + +static bool string_is_only_whitespace(const char *str, size_t size) { + for(size_t i = 0; i < size; ++i) { + if(str[i] != ' ' && str[i] != '\t' && str[i] != '\r' && str[i] != '\n') + return false; + } + return true; +} + +static bool fd_set_cloexec(int fd) { + const int flags = fcntl(fd, F_GETFD); + return flags != -1 && fcntl(fd, F_SETFD, flags | FD_CLOEXEC) != -1; +} + +static bool fd_set_nonblocking(int fd) { + const int flags = fcntl(fd, F_GETFL); + return flags != -1 && fcntl(fd, F_SETFL, flags | O_NONBLOCK) != -1; +} + +#ifdef __linux__ +static bool ipc_poller_init(gsr_ipc *self) { + self->poll_fd = epoll_create1(EPOLL_CLOEXEC); + if(self->poll_fd == -1) { + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_ipc: failed to create an epoll instance, error: %s", strerror(errno)); + return false; + } + return true; +} + +static bool ipc_poller_add(gsr_ipc *self, int fd) { + struct epoll_event event; + memset(&event, 0, sizeof(event)); + event.events = EPOLLIN | EPOLLET; + event.data.fd = fd; + return epoll_ctl(self->poll_fd, EPOLL_CTL_ADD, fd, &event) == 0; +} + +static bool ipc_poller_set_write_notify(gsr_ipc *self, int fd, bool enable) { + struct epoll_event event; + memset(&event, 0, sizeof(event)); + event.events = EPOLLIN | EPOLLET | (enable ? EPOLLOUT : 0); + event.data.fd = fd; + return epoll_ctl(self->poll_fd, EPOLL_CTL_MOD, fd, &event) == 0; +} + +/* Returns the number of events, or -1 on failure. Waits until at least one event is available */ +static int ipc_poller_wait(gsr_ipc *self, gsr_ipc_event *events, int events_capacity) { + struct epoll_event platform_events[GSR_IPC_MAX_EVENTS]; + if(events_capacity > GSR_IPC_MAX_EVENTS) + events_capacity = GSR_IPC_MAX_EVENTS; + + const int num_events = epoll_wait(self->poll_fd, platform_events, events_capacity, -1); + if(num_events == -1) + return errno == EINTR ? 0 : -1; + + for(int i = 0; i < num_events; ++i) { + events[i].fd = platform_events[i].data.fd; + events[i].readable = platform_events[i].events & (EPOLLIN | EPOLLHUP | EPOLLERR); + events[i].writable = platform_events[i].events & EPOLLOUT; + } + return num_events; +} +#else +static bool ipc_poller_init(gsr_ipc *self) { + self->poll_fd = kqueue(); + if(self->poll_fd == -1) { + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_ipc: failed to create a kqueue instance, error: %s", strerror(errno)); + return false; + } + fd_set_cloexec(self->poll_fd); + return true; +} + +static bool ipc_poller_add(gsr_ipc *self, int fd) { + struct kevent change; + EV_SET(&change, fd, EVFILT_READ, EV_ADD | EV_CLEAR, 0, 0, NULL); + return kevent(self->poll_fd, &change, 1, NULL, 0, NULL) != -1; +} + +static bool ipc_poller_set_write_notify(gsr_ipc *self, int fd, bool enable) { + struct kevent change; + EV_SET(&change, fd, EVFILT_WRITE, enable ? (EV_ADD | EV_CLEAR) : EV_DELETE, 0, 0, NULL); + return kevent(self->poll_fd, &change, 1, NULL, 0, NULL) != -1; +} + +/* Returns the number of events, or -1 on failure. Waits until at least one event is available */ +static int ipc_poller_wait(gsr_ipc *self, gsr_ipc_event *events, int events_capacity) { + struct kevent platform_events[GSR_IPC_MAX_EVENTS]; + if(events_capacity > GSR_IPC_MAX_EVENTS) + events_capacity = GSR_IPC_MAX_EVENTS; + + const int num_events = kevent(self->poll_fd, NULL, 0, platform_events, events_capacity, NULL); + if(num_events == -1) + return errno == EINTR ? 0 : -1; + + for(int i = 0; i < num_events; ++i) { + events[i].fd = platform_events[i].ident; + events[i].readable = platform_events[i].filter == EVFILT_READ; + events[i].writable = platform_events[i].filter == EVFILT_WRITE; + } + return num_events; +} +#endif + +/* Only used when the ipc thread exits, to not lose replies that haven't been fully sent yet */ +static bool ipc_send_all_blocking(int fd, const char *data, size_t size) { + size_t offset = 0; + while(offset < size) { + const ssize_t bytes_written = send(fd, data + offset, size - offset, MSG_NOSIGNAL); + if(bytes_written > 0) { + offset += bytes_written; + continue; + } + + if(bytes_written == -1 && errno == EINTR) + continue; + + if(bytes_written == -1 && (errno == EAGAIN || errno == EWOULDBLOCK)) { + struct pollfd poll_fd; + poll_fd.fd = fd; + poll_fd.events = POLLOUT; + poll_fd.revents = 0; + + const int poll_result = poll(&poll_fd, 1, GSR_IPC_SHUTDOWN_SEND_TIMEOUT_MILLISECONDS); + if(poll_result == -1 && errno == EINTR) + continue; + + if(poll_result <= 0) + return false; + + continue; + } + + return false; + } + return true; +} + +static bool ipc_client_send_data(gsr_ipc *self, gsr_ipc_client *client, const char *data, size_t size) { + size_t offset = 0; + if(client->send_buffer_size == 0) { + while(offset < size) { + const ssize_t bytes_written = send(client->fd, data + offset, size - offset, MSG_NOSIGNAL); + if(bytes_written > 0) { + offset += bytes_written; + continue; + } + + if(bytes_written == -1 && errno == EINTR) + continue; + + if(bytes_written == -1 && (errno == EAGAIN || errno == EWOULDBLOCK)) + break; + + return false; + } + } + + const size_t bytes_remaining = size - offset; + if(bytes_remaining == 0) + return true; + + if(client->send_buffer_size + bytes_remaining > sizeof(client->send_buffer)) { + gsr_log(GSR_LOG_LEVEL_WARNING, "gsr_ipc: an ipc client isn't reading replies fast enough, disconnecting it"); + return false; + } + + const bool send_buffer_was_empty = client->send_buffer_size == 0; + memcpy(client->send_buffer + client->send_buffer_size, data + offset, bytes_remaining); + client->send_buffer_size += bytes_remaining; + return !send_buffer_was_empty || ipc_poller_set_write_notify(self, client->fd, true); +} + +static bool ipc_client_flush_send_buffer(gsr_ipc *self, gsr_ipc_client *client) { + if(client->send_buffer_size == 0) + return true; + + size_t offset = 0; + while(offset < client->send_buffer_size) { + const ssize_t bytes_written = send(client->fd, client->send_buffer + offset, client->send_buffer_size - offset, MSG_NOSIGNAL); + if(bytes_written > 0) { + offset += bytes_written; + continue; + } + + if(bytes_written == -1 && errno == EINTR) + continue; + + if(bytes_written == -1 && (errno == EAGAIN || errno == EWOULDBLOCK)) + break; + + return false; + } + + memmove(client->send_buffer, client->send_buffer + offset, client->send_buffer_size - offset); + client->send_buffer_size -= offset; + return client->send_buffer_size != 0 || ipc_poller_set_write_notify(self, client->fd, false); +} + +static bool ipc_client_send_reply(gsr_ipc *self, gsr_ipc_client *client, int64_t id, bool success, const char *error_message, const char *data) { + char reply[GSR_IPC_MAX_REPLY_SIZE]; + int reply_size = 0; + + if(success && data) { + char escaped_data[GSR_IPC_MAX_ESCAPED_DATA_SIZE]; + gsr_json_escape_string(escaped_data, sizeof(escaped_data), data); + reply_size = snprintf(reply, sizeof(reply), "{\"id\":%" PRIi64 ",\"result\":\"ok\",\"data\":\"%s\"}\n", id, escaped_data); + } else if(success) { + reply_size = snprintf(reply, sizeof(reply), "{\"id\":%" PRIi64 ",\"result\":\"ok\"}\n", id); + } else { + char escaped_error_message[GSR_IPC_MAX_ESCAPED_ERROR_MESSAGE_SIZE]; + gsr_json_escape_string(escaped_error_message, sizeof(escaped_error_message), error_message ? error_message : ""); + reply_size = snprintf(reply, sizeof(reply), "{\"id\":%" PRIi64 ",\"result\":\"error\",\"data\":\"%s\"}\n", id, escaped_error_message); + } + + if(reply_size < 0 || reply_size >= (int)sizeof(reply)) { + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_ipc: failed to create a reply to request %" PRIi64, id); + return false; + } + + return ipc_client_send_data(self, client, reply, reply_size); +} + +static bool ipc_request_parse(char *data, size_t size, gsr_ipc_request *request, char *error_message, size_t error_message_size) { + memset(request, 0, sizeof(*request)); + request->request_end = data + size; + + sj_Reader reader = sj_reader(data, size); + const sj_Value root = sj_read(&reader); + if(root.type != SJ_OBJECT) { + snprintf(error_message, error_message_size, "expected the request to be a json object"); + return false; + } + + sj_Value id_value; + sj_Value name_value; + bool has_id = false; + bool has_name = false; + + sj_Value key; + sj_Value value; + while(sj_iter_object(&reader, root, &key, &value)) { + if(gsr_json_string_equals(&key, "id")) { + id_value = value; + has_id = true; + } else if(gsr_json_string_equals(&key, "name")) { + name_value = value; + has_name = true; + } else if(gsr_json_string_equals(&key, "data")) { + request->data = value; + request->has_data = true; + } + } + + if(reader.error) { + snprintf(error_message, error_message_size, "failed to parse the request: %s", reader.error); + return false; + } + + if(!has_id) { + snprintf(error_message, error_message_size, "the request is missing the 'id' field"); + return false; + } + + if(!gsr_json_number_to_int64(&id_value, &request->id)) { + snprintf(error_message, error_message_size, "expected 'id' to be an integer"); + return false; + } + + if(!has_name) { + snprintf(error_message, error_message_size, "the request is missing the 'name' field"); + return false; + } + + if(name_value.type != SJ_STRING) { + snprintf(error_message, error_message_size, "expected 'name' to be a string"); + return false; + } + + snprintf(request->name, sizeof(request->name), "%.*s", (int)(name_value.end - name_value.start), name_value.start); + return true; +} + +static bool json_value_to_save_replay_seconds(const sj_Value *value, int *seconds, char *error_message, size_t error_message_size) { + int64_t data_seconds = 0; + if(!gsr_json_number_to_int64(value, &data_seconds) || data_seconds <= 0 || data_seconds > INT_MAX) { + snprintf(error_message, error_message_size, "expected the number of seconds to save to be larger than 0"); + return false; + } + + *seconds = data_seconds; + return true; +} + +static bool ipc_request_get_save_replay_options(const gsr_ipc_request *request, int *seconds, bool *has_restart_replay, bool *restart_replay, char *error_message, size_t error_message_size) { + *seconds = GSR_SAVE_REPLAY_SECONDS_FULL; + *has_restart_replay = false; + *restart_replay = false; + if(!request->has_data || request->data.type == SJ_NULL) + return true; + + if(request->data.type != SJ_OBJECT) { + snprintf(error_message, error_message_size, "expected 'data' to be an object with the optional fields 'seconds' and 'restart-replay'"); + return false; + } + + sj_Reader reader = sj_reader(request->data.start, request->request_end - request->data.start); + const sj_Value data = sj_read(&reader); + + sj_Value key; + sj_Value value; + while(sj_iter_object(&reader, data, &key, &value)) { + if(gsr_json_string_equals(&key, "seconds")) { + if(value.type == SJ_NULL) + continue; + + if(!json_value_to_save_replay_seconds(&value, seconds, error_message, error_message_size)) + return false; + } else if(gsr_json_string_equals(&key, "restart-replay")) { + if(value.type != SJ_BOOL) { + snprintf(error_message, error_message_size, "expected 'restart-replay' to be true or false"); + return false; + } + + *has_restart_replay = true; + *restart_replay = gsr_json_string_equals(&value, "true"); + } + } + + if(reader.error) { + snprintf(error_message, error_message_size, "failed to parse 'data': %s", reader.error); + return false; + } + + return true; +} + +static bool ipc_request_get_set_paused_state(const gsr_ipc_request *request, bool *paused, char *error_message, size_t error_message_size) { + if(request->has_data && request->data.type == SJ_BOOL) { + *paused = gsr_json_string_equals(&request->data, "true"); + return true; + } + + snprintf(error_message, error_message_size, "expected 'data' to be true to pause or false to unpause"); + return false; +} + +static bool ipc_request_name_to_deferred_request_type(const char *name, gsr_ipc_deferred_request_type *type) { + if(strcmp(name, "stop") == 0) { + *type = GSR_IPC_DEFERRED_REQUEST_STOP; + return true; + } + + if(strcmp(name, "save-replay") == 0) { + *type = GSR_IPC_DEFERRED_REQUEST_SAVE_REPLAY; + return true; + } + + if(strcmp(name, "stop-replay-recording") == 0) { + *type = GSR_IPC_DEFERRED_REQUEST_STOP_REPLAY_RECORDING; + return true; + } + + return false; +} + +static const char* deferred_request_already_pending_error(gsr_ipc_deferred_request_type type) { + switch(type) { + case GSR_IPC_DEFERRED_REQUEST_STOP: return "GPU Screen Recorder is already stopping"; + case GSR_IPC_DEFERRED_REQUEST_SAVE_REPLAY: return "a replay is already being saved"; + case GSR_IPC_DEFERRED_REQUEST_STOP_REPLAY_RECORDING: return "the recording is already being stopped"; + case GSR_IPC_DEFERRED_REQUEST_TYPE_COUNT: break; + } + return "the request is already being handled"; +} + +static const char* deferred_request_failed_error(gsr_ipc_deferred_request_type type) { + switch(type) { + case GSR_IPC_DEFERRED_REQUEST_STOP: return "failed to save the recording"; + case GSR_IPC_DEFERRED_REQUEST_SAVE_REPLAY: return "failed to save the replay"; + case GSR_IPC_DEFERRED_REQUEST_STOP_REPLAY_RECORDING: return "failed to save the recording"; + case GSR_IPC_DEFERRED_REQUEST_TYPE_COUNT: break; + } + return "the request failed"; +} + +static bool ipc_set_deferred_request_pending(gsr_ipc *self, gsr_ipc_deferred_request_type type, int client_fd, int64_t request_id) { + pthread_mutex_lock(&self->deferred_requests_mutex); + gsr_ipc_deferred_request *deferred_request = &self->deferred_requests[type]; + const bool was_empty = deferred_request->state == GSR_IPC_DEFERRED_REQUEST_STATE_EMPTY; + if(was_empty) { + deferred_request->state = GSR_IPC_DEFERRED_REQUEST_STATE_PENDING; + deferred_request->client_fd = client_fd; + deferred_request->request_id = request_id; + deferred_request->success = false; + deferred_request->has_filepath = false; + } + pthread_mutex_unlock(&self->deferred_requests_mutex); + return was_empty; +} + +static void ipc_clear_deferred_request(gsr_ipc *self, gsr_ipc_deferred_request_type type) { + pthread_mutex_lock(&self->deferred_requests_mutex); + self->deferred_requests[type].state = GSR_IPC_DEFERRED_REQUEST_STATE_EMPTY; + pthread_mutex_unlock(&self->deferred_requests_mutex); +} + +static bool ipc_handle_request(gsr_ipc *self, const gsr_ipc_request *request, char *error_message, size_t error_message_size) { + if(strcmp(request->name, "stop") == 0) + return self->handlers.stop(error_message, error_message_size, self->handlers.userdata); + + if(strcmp(request->name, "toggle-pause") == 0) + return self->handlers.toggle_pause(error_message, error_message_size, self->handlers.userdata); + + if(strcmp(request->name, "set-paused") == 0) { + bool paused = false; + if(!ipc_request_get_set_paused_state(request, &paused, error_message, error_message_size)) + return false; + + return self->handlers.set_paused(paused, error_message, error_message_size, self->handlers.userdata); + } + + if(strcmp(request->name, "toggle-replay-recording") == 0) + return self->handlers.toggle_replay_recording(error_message, error_message_size, self->handlers.userdata); + + if(strcmp(request->name, "start-replay-recording") == 0) + return self->handlers.start_replay_recording(error_message, error_message_size, self->handlers.userdata); + + if(strcmp(request->name, "stop-replay-recording") == 0) + return self->handlers.stop_replay_recording(error_message, error_message_size, self->handlers.userdata); + + if(strcmp(request->name, "save-replay") == 0) { + int seconds = GSR_SAVE_REPLAY_SECONDS_FULL; + bool has_restart_replay = false; + bool restart_replay = false; + if(!ipc_request_get_save_replay_options(request, &seconds, &has_restart_replay, &restart_replay, error_message, error_message_size)) + return false; + + return self->handlers.save_replay(seconds, has_restart_replay, restart_replay, error_message, error_message_size, self->handlers.userdata); + } + + snprintf(error_message, error_message_size, "unknown request name '%s'", request->name); + return false; +} + +static bool ipc_client_on_request(gsr_ipc *self, gsr_ipc_client *client) { + if(client->request_too_large) + return ipc_client_send_reply(self, client, 0, false, "the request is too large", NULL); + + if(string_is_only_whitespace(client->request, client->request_size)) + return ipc_client_send_reply(self, client, 0, false, "the request is empty", NULL); + + char error_message[GSR_IPC_MAX_ERROR_MESSAGE_SIZE]; + error_message[0] = '\0'; + + gsr_ipc_request request; + if(!ipc_request_parse(client->request, client->request_size, &request, error_message, sizeof(error_message))) + return ipc_client_send_reply(self, client, request.id, false, error_message, NULL); + + /* The pending deferred request has to be registered before the handler starts the operation, + otherwise the operation could finish before the reply to it gets registered */ + gsr_ipc_deferred_request_type deferred_request_type; + const bool reply_is_deferred = ipc_request_name_to_deferred_request_type(request.name, &deferred_request_type); + if(reply_is_deferred && !ipc_set_deferred_request_pending(self, deferred_request_type, client->fd, request.id)) + return ipc_client_send_reply(self, client, request.id, false, deferred_request_already_pending_error(deferred_request_type), NULL); + + if(!ipc_handle_request(self, &request, error_message, sizeof(error_message))) { + if(reply_is_deferred) + ipc_clear_deferred_request(self, deferred_request_type); + return ipc_client_send_reply(self, client, request.id, false, error_message, NULL); + } + + if(reply_is_deferred) + return true; + + return ipc_client_send_reply(self, client, request.id, true, NULL, NULL); +} + +static bool ipc_client_on_byte(gsr_ipc *self, gsr_ipc_client *client, char c) { + if(c != '\n') { + if(client->request_size < GSR_IPC_MAX_REQUEST_SIZE) + client->request[client->request_size++] = c; + else + client->request_too_large = true; + return true; + } + + const bool keep_client = ipc_client_on_request(self, client); + client->request_size = 0; + client->request_too_large = false; + return keep_client; +} + +static bool ipc_client_receive(gsr_ipc *self, gsr_ipc_client *client) { + for(;;) { + char buffer[1024]; + const ssize_t bytes_read = recv(client->fd, buffer, sizeof(buffer), 0); + if(bytes_read == 0) + return false; + + if(bytes_read == -1) { + if(errno == EINTR) + continue; + + return errno == EAGAIN || errno == EWOULDBLOCK; + } + + for(ssize_t i = 0; i < bytes_read; ++i) { + if(!ipc_client_on_byte(self, client, buffer[i])) + return false; + } + } +} + +static void ipc_add_client(gsr_ipc *self, int client_fd) { + if(self->num_clients == GSR_IPC_MAX_CLIENTS) { + gsr_log(GSR_LOG_LEVEL_WARNING, "gsr_ipc: too many ipc clients are connected, rejecting the new connection"); + close(client_fd); + return; + } + + if(!fd_set_cloexec(client_fd) || !fd_set_nonblocking(client_fd) || !ipc_poller_add(self, client_fd)) { + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_ipc: failed to setup the ipc client socket, error: %s", strerror(errno)); + close(client_fd); + return; + } + + gsr_ipc_client *client = &self->clients[self->num_clients]; + client->fd = client_fd; + client->request_size = 0; + client->request_too_large = false; + client->send_buffer_size = 0; + ++self->num_clients; +} + +static void ipc_accept_clients(gsr_ipc *self) { + for(;;) { + const int client_fd = accept(self->socket_fd, NULL, NULL); + if(client_fd == -1) { + if(errno == EINTR) + continue; + return; + } + + ipc_add_client(self, client_fd); + } +} + +static void ipc_remove_client(gsr_ipc *self, int index) { + const int client_fd = self->clients[index].fd; + + pthread_mutex_lock(&self->deferred_requests_mutex); + for(int i = 0; i < GSR_IPC_DEFERRED_REQUEST_TYPE_COUNT; ++i) { + if(self->deferred_requests[i].state != GSR_IPC_DEFERRED_REQUEST_STATE_EMPTY && self->deferred_requests[i].client_fd == client_fd) + self->deferred_requests[i].state = GSR_IPC_DEFERRED_REQUEST_STATE_EMPTY; + } + pthread_mutex_unlock(&self->deferred_requests_mutex); + + close(client_fd); + for(int i = index; i < self->num_clients - 1; ++i) { + self->clients[i] = self->clients[i + 1]; + } + --self->num_clients; +} + +static int ipc_find_client_index_by_fd(const gsr_ipc *self, int fd) { + for(int i = 0; i < self->num_clients; ++i) { + if(self->clients[i].fd == fd) + return i; + } + return -1; +} + +static void ipc_send_completed_request_replies(gsr_ipc *self) { + for(int i = 0; i < GSR_IPC_DEFERRED_REQUEST_TYPE_COUNT; ++i) { + pthread_mutex_lock(&self->deferred_requests_mutex); + const gsr_ipc_deferred_request deferred_request = self->deferred_requests[i]; + if(deferred_request.state == GSR_IPC_DEFERRED_REQUEST_STATE_COMPLETED) + self->deferred_requests[i].state = GSR_IPC_DEFERRED_REQUEST_STATE_EMPTY; + pthread_mutex_unlock(&self->deferred_requests_mutex); + + if(deferred_request.state != GSR_IPC_DEFERRED_REQUEST_STATE_COMPLETED) + continue; + + const int client_index = ipc_find_client_index_by_fd(self, deferred_request.client_fd); + if(client_index == -1) + continue; + + const char *error_message = deferred_request_failed_error(i); + const char *filepath = deferred_request.has_filepath ? deferred_request.filepath : NULL; + if(!ipc_client_send_reply(self, &self->clients[client_index], deferred_request.request_id, deferred_request.success, error_message, filepath)) + ipc_remove_client(self, client_index); + } +} + +static void ipc_fail_pending_requests(gsr_ipc *self) { + for(int i = 0; i < GSR_IPC_DEFERRED_REQUEST_TYPE_COUNT; ++i) { + pthread_mutex_lock(&self->deferred_requests_mutex); + const gsr_ipc_deferred_request deferred_request = self->deferred_requests[i]; + self->deferred_requests[i].state = GSR_IPC_DEFERRED_REQUEST_STATE_EMPTY; + pthread_mutex_unlock(&self->deferred_requests_mutex); + + if(deferred_request.state != GSR_IPC_DEFERRED_REQUEST_STATE_PENDING) + continue; + + const int client_index = ipc_find_client_index_by_fd(self, deferred_request.client_fd); + if(client_index == -1) + continue; + + if(!ipc_client_send_reply(self, &self->clients[client_index], deferred_request.request_id, false, "GPU Screen Recorder exited before the request finished", NULL)) + ipc_remove_client(self, client_index); + } +} + +static void ipc_flush_clients_blocking(gsr_ipc *self) { + for(int i = 0; i < self->num_clients; ++i) { + gsr_ipc_client *client = &self->clients[i]; + if(client->send_buffer_size > 0) + ipc_send_all_blocking(client->fd, client->send_buffer, client->send_buffer_size); + client->send_buffer_size = 0; + } +} + +static int ipc_drain_wakeup_pipe(gsr_ipc *self) { + int wakeup_flags = 0; + for(;;) { + char buffer[64]; + const ssize_t bytes_read = read(self->wakeup_pipe[0], buffer, sizeof(buffer)); + if(bytes_read == -1 && errno == EINTR) + continue; + + if(bytes_read <= 0) + break; + + for(ssize_t i = 0; i < bytes_read; ++i) { + if(buffer[i] == 'q') + wakeup_flags |= GSR_IPC_WAKEUP_QUIT; + else if(buffer[i] == 'c') + wakeup_flags |= GSR_IPC_WAKEUP_COMPLETED_REQUEST; + } + } + return wakeup_flags; +} + +static void ipc_wakeup_thread(gsr_ipc *self, char wakeup_value) { + ssize_t bytes_written = 0; + do { + bytes_written = write(self->wakeup_pipe[1], &wakeup_value, 1); + } while(bytes_written == -1 && errno == EINTR); + + if(bytes_written == -1) + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_ipc: failed to wake up the ipc thread, error: %s", strerror(errno)); +} + +static void* ipc_thread(void *userdata) { + gsr_ipc *self = userdata; + gsr_ipc_event events[GSR_IPC_MAX_EVENTS]; + bool running = true; + + while(running) { + const int num_events = ipc_poller_wait(self, events, GSR_IPC_MAX_EVENTS); + if(num_events == -1) { + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_ipc: failed to wait for ipc events, error: %s", strerror(errno)); + break; + } + + for(int i = 0; i < num_events; ++i) { + if(events[i].fd == self->wakeup_pipe[0]) { + const int wakeup_flags = ipc_drain_wakeup_pipe(self); + if(wakeup_flags & GSR_IPC_WAKEUP_COMPLETED_REQUEST) + ipc_send_completed_request_replies(self); + if(wakeup_flags & GSR_IPC_WAKEUP_QUIT) + running = false; + continue; + } + + if(events[i].fd == self->socket_fd) { + ipc_accept_clients(self); + continue; + } + + const int client_index = ipc_find_client_index_by_fd(self, events[i].fd); + if(client_index == -1) + continue; + + gsr_ipc_client *client = &self->clients[client_index]; + bool keep_client = true; + if(events[i].readable) + keep_client = ipc_client_receive(self, client); + if(keep_client && events[i].writable) + keep_client = ipc_client_flush_send_buffer(self, client); + + if(!keep_client) + ipc_remove_client(self, client_index); + } + } + + ipc_send_completed_request_replies(self); + ipc_fail_pending_requests(self); + ipc_flush_clients_blocking(self); + return NULL; +} + +static bool ipc_socket_filepath_in_use(const char *socket_filepath) { + struct sockaddr_un addr; + memset(&addr, 0, sizeof(addr)); + addr.sun_family = AF_UNIX; + snprintf(addr.sun_path, sizeof(addr.sun_path), "%s", socket_filepath); + + const int fd = socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0); + if(fd == -1) + return true; + + const bool in_use = connect(fd, (const struct sockaddr*)&addr, sizeof(addr)) == 0; + close(fd); + return in_use; +} + +/* Removes the socket that a GPU Screen Recorder instance that was killed left behind, so the same ipc socket path can be used again */ +static bool ipc_remove_unused_socket(const char *socket_filepath) { + struct stat file_stat; + if(lstat(socket_filepath, &file_stat) == -1) + return true; + + if(!S_ISSOCK(file_stat.st_mode)) { + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_ipc_init: can't use \"%s\" as the ipc socket path because a file that isn't a socket already exists there", socket_filepath); + return false; + } + + if(ipc_socket_filepath_in_use(socket_filepath)) { + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_ipc_init: another program is already listening on \"%s\"", socket_filepath); + return false; + } + + unlink(socket_filepath); + return true; +} + +static bool ipc_bind(gsr_ipc *self, const struct sockaddr_un *addr) { + if(!ipc_remove_unused_socket(self->socket_filepath)) + return false; + + const mode_t prev_mask = umask(0777 & ~GSR_IPC_SOCKET_MODE); + const int bind_result = bind(self->socket_fd, (const struct sockaddr*)addr, sizeof(*addr)); + const int bind_error = errno; + umask(prev_mask); + + if(bind_result == -1) { + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_ipc_init: failed to bind the ipc socket to \"%s\", error: %s", self->socket_filepath, strerror(bind_error)); + return false; + } + + self->socket_bound = true; + return true; +} + +static void ipc_close(gsr_ipc *self) { + for(int i = 0; i < self->num_clients; ++i) { + close(self->clients[i].fd); + } + self->num_clients = 0; + + for(int i = 0; i < 2; ++i) { + if(self->wakeup_pipe[i] != -1) { + close(self->wakeup_pipe[i]); + self->wakeup_pipe[i] = -1; + } + } + + if(self->poll_fd != -1) { + close(self->poll_fd); + self->poll_fd = -1; + } + + if(self->socket_fd != -1) { + close(self->socket_fd); + self->socket_fd = -1; + } + + if(self->socket_bound) { + unlink(self->socket_filepath); + self->socket_bound = false; + } + + if(self->deferred_requests_mutex_created) { + pthread_mutex_destroy(&self->deferred_requests_mutex); + self->deferred_requests_mutex_created = false; + } +} + +int gsr_ipc_init(gsr_ipc *self, const char *socket_filepath) { + memset(self, 0, sizeof(*self)); + self->socket_fd = -1; + self->poll_fd = -1; + self->wakeup_pipe[0] = -1; + self->wakeup_pipe[1] = -1; + + struct sockaddr_un addr; + memset(&addr, 0, sizeof(addr)); + addr.sun_family = AF_UNIX; + if(snprintf(addr.sun_path, sizeof(addr.sun_path), "%s", socket_filepath) >= (int)sizeof(addr.sun_path)) { + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_ipc_init: the ipc socket path is too long, it can be at most %d characters: \"%s\"", (int)sizeof(addr.sun_path) - 1, socket_filepath); + goto err; + } + + snprintf(self->socket_filepath, sizeof(self->socket_filepath), "%s", socket_filepath); + + if(pthread_mutex_init(&self->deferred_requests_mutex, NULL) != 0) { + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_ipc_init: failed to create the deferred requests mutex"); + goto err; + } + self->deferred_requests_mutex_created = true; + + if(pipe(self->wakeup_pipe) == -1) { + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_ipc_init: failed to create the ipc wakeup pipe, error: %s", strerror(errno)); + self->wakeup_pipe[0] = -1; + self->wakeup_pipe[1] = -1; + goto err; + } + + if(!fd_set_cloexec(self->wakeup_pipe[0]) || !fd_set_cloexec(self->wakeup_pipe[1]) || !fd_set_nonblocking(self->wakeup_pipe[0])) { + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_ipc_init: failed to setup the ipc wakeup pipe, error: %s", strerror(errno)); + goto err; + } + + self->socket_fd = socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC | SOCK_NONBLOCK, 0); + if(self->socket_fd == -1) { + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_ipc_init: failed to create the ipc socket, error: %s", strerror(errno)); + goto err; + } + + if(!ipc_poller_init(self)) + goto err; + + if(!ipc_poller_add(self, self->wakeup_pipe[0]) || !ipc_poller_add(self, self->socket_fd)) { + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_ipc_init: failed to register the ipc sockets for events, error: %s", strerror(errno)); + goto err; + } + + if(!ipc_bind(self, &addr)) + goto err; + + if(listen(self->socket_fd, GSR_IPC_MAX_CLIENTS) == -1) { + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_ipc_init: failed to listen on the ipc socket, error: %s", strerror(errno)); + goto err; + } + + self->initialized = true; + return GSR_ERROR_OK; + + err: + ipc_close(self); + return GSR_ERROR_GENERIC; +} + +void gsr_ipc_deinit(gsr_ipc *self) { + if(!self->initialized) + return; + + gsr_ipc_stop(self); + ipc_close(self); + self->initialized = false; +} + +int gsr_ipc_start(gsr_ipc *self, const gsr_ipc_handlers *handlers) { + if(!self->initialized) + return GSR_ERROR_OK; + + self->handlers = *handlers; + + /* Block all signals in the ipc thread to keep the signal handlers running on the main thread */ + sigset_t all_signals; + sigset_t prev_signals; + sigfillset(&all_signals); + pthread_sigmask(SIG_SETMASK, &all_signals, &prev_signals); + const int thread_create_result = pthread_create(&self->thread, NULL, ipc_thread, self); + pthread_sigmask(SIG_SETMASK, &prev_signals, NULL); + + if(thread_create_result != 0) { + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_ipc_start: failed to create the ipc thread, error: %s", strerror(thread_create_result)); + return GSR_ERROR_GENERIC; + } + + self->thread_running = true; + return GSR_ERROR_OK; +} + +void gsr_ipc_stop(gsr_ipc *self) { + if(!self->thread_running) + return; + + ipc_wakeup_thread(self, 'q'); + pthread_join(self->thread, NULL); + self->thread_running = false; +} + +void gsr_ipc_complete_request(gsr_ipc *self, gsr_ipc_deferred_request_type type, bool success, const char *filepath) { + if(!self->initialized) + return; + + pthread_mutex_lock(&self->deferred_requests_mutex); + gsr_ipc_deferred_request *deferred_request = &self->deferred_requests[type]; + const bool was_pending = deferred_request->state == GSR_IPC_DEFERRED_REQUEST_STATE_PENDING; + if(was_pending) { + deferred_request->state = GSR_IPC_DEFERRED_REQUEST_STATE_COMPLETED; + deferred_request->success = success; + deferred_request->has_filepath = filepath != NULL; + if(filepath) + snprintf(deferred_request->filepath, sizeof(deferred_request->filepath), "%s", filepath); + } + pthread_mutex_unlock(&self->deferred_requests_mutex); + + if(was_pending) + ipc_wakeup_thread(self, 'c'); +} diff --git a/src/cli/main.c b/src/cli/main.c new file mode 100644 index 0000000..aaa315c --- /dev/null +++ b/src/cli/main.c @@ -0,0 +1,708 @@ +#include "../../include/cli/commands.h" +#include "../../include/cli/ipc.h" +#include "../../include/recorder/recorder.h" +#include "../../include/recorder/screenshot.h" +#include "../../include/recorder/capture_source.h" +#include "../../include/recorder/capture_setup.h" +#include "../../include/recorder/windowing.h" +#include "../../include/recorder/audio_input.h" +#include "../../include/recorder/replay_save.h" +#include "../../include/recorder/error.h" +#include "../../include/args_parser.h" +#include "../../include/sound.h" +#include "../../include/shader.h" +#include "../../include/utils.h" +#include "../../include/log.h" +#ifdef GSR_APP_AUDIO +#include "../../include/pipewire_audio.h" +#endif + +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <assert.h> +#include <locale.h> +#include <signal.h> +#include <stdatomic.h> +#include <unistd.h> +#include <malloc.h> + +static atomic_int running = 1; +static gsr_recorder *recorder = NULL; +/* Signals that are received before the recorder has been created are applied when it has been created */ +static volatile sig_atomic_t pending_toggle_pause = 0; +static volatile sig_atomic_t pending_toggle_replay_recording = 0; +static volatile sig_atomic_t pending_save_replay_seconds = 0; + +static void stop_handler(int signal_value) { + (void)signal_value; + atomic_store(&running, 0); + if(recorder) + gsr_recorder_stop(recorder); +} + +static void toggle_pause_handler(int signal_value) { + (void)signal_value; + if(recorder) + gsr_recorder_toggle_pause(recorder); + else + pending_toggle_pause = 1; +} + +static void toggle_replay_recording_handler(int signal_value) { + (void)signal_value; + if(recorder) + gsr_recorder_toggle_replay_recording(recorder); + else + pending_toggle_replay_recording = 1; +} + +static void save_replay_seconds_handler(gsr_recorder *rec, int seconds) { + if(rec) + gsr_recorder_save_replay(rec, seconds, GSR_RESTART_REPLAY_USE_OPTION); + else + pending_save_replay_seconds = seconds; +} + +static void apply_pending_signals(gsr_recorder *rec) { + if(pending_toggle_pause) { + pending_toggle_pause = 0; + gsr_recorder_toggle_pause(rec); + } + + if(pending_toggle_replay_recording) { + pending_toggle_replay_recording = 0; + gsr_recorder_toggle_replay_recording(rec); + } + + if(pending_save_replay_seconds != 0) { + const int seconds = pending_save_replay_seconds; + pending_save_replay_seconds = 0; + gsr_recorder_save_replay(rec, seconds, GSR_RESTART_REPLAY_USE_OPTION); + } +} + +static void save_replay_handler(int signal_value) { + (void)signal_value; + save_replay_seconds_handler(recorder, GSR_SAVE_REPLAY_SECONDS_FULL); +} + +static void save_replay_10_seconds_handler(int signal_value) { + (void)signal_value; + save_replay_seconds_handler(recorder, 10); +} + +static void save_replay_30_seconds_handler(int signal_value) { + (void)signal_value; + save_replay_seconds_handler(recorder, 30); +} + +static void save_replay_1_minute_handler(int signal_value) { + (void)signal_value; + save_replay_seconds_handler(recorder, 60); +} + +static void save_replay_5_minutes_handler(int signal_value) { + (void)signal_value; + save_replay_seconds_handler(recorder, 60*5); +} + +static void save_replay_10_minutes_handler(int signal_value) { + (void)signal_value; + save_replay_seconds_handler(recorder, 60*10); +} + +static void save_replay_30_minutes_handler(int signal_value) { + (void)signal_value; + save_replay_seconds_handler(recorder, 60*30); +} + +static bool ipc_stop_handler(char *error_message, size_t error_message_size, void *userdata) { + (void)error_message; + (void)error_message_size; + (void)userdata; + atomic_store(&running, 0); + gsr_recorder_stop(recorder); + return true; +} + +static bool ipc_toggle_pause_handler(char *error_message, size_t error_message_size, void *userdata) { + const gsr_recorder_settings *settings = userdata; + if(settings->is_replaying) { + snprintf(error_message, error_message_size, "pausing is not supported when recording a replay"); + return false; + } + + gsr_recorder_toggle_pause(recorder); + return true; +} + +static bool ipc_set_paused_handler(bool paused, char *error_message, size_t error_message_size, void *userdata) { + const gsr_recorder_settings *settings = userdata; + if(settings->is_replaying) { + snprintf(error_message, error_message_size, "pausing is not supported when recording a replay"); + return false; + } + + gsr_recorder_set_paused(recorder, paused); + return true; +} + +static bool ipc_toggle_replay_recording_handler(char *error_message, size_t error_message_size, void *userdata) { + const gsr_recorder_settings *settings = userdata; + if(!settings->replay_recording_directory) { + snprintf(error_message, error_message_size, "option -ro is required to start a recording"); + return false; + } + + gsr_recorder_toggle_replay_recording(recorder); + return true; +} + +static bool ipc_start_replay_recording_handler(char *error_message, size_t error_message_size, void *userdata) { + const gsr_recorder_settings *settings = userdata; + if(!settings->replay_recording_directory) { + snprintf(error_message, error_message_size, "option -ro is required to start a recording"); + return false; + } + + gsr_recorder_start_replay_recording(recorder); + return true; +} + +static bool ipc_stop_replay_recording_handler(char *error_message, size_t error_message_size, void *userdata) { + const gsr_recorder_settings *settings = userdata; + if(!settings->replay_recording_directory) { + snprintf(error_message, error_message_size, "option -ro is required to start a recording"); + return false; + } + + if(!gsr_recorder_is_replay_recording(recorder)) { + snprintf(error_message, error_message_size, "no recording is running"); + return false; + } + + gsr_recorder_stop_replay_recording(recorder); + return true; +} + +static bool ipc_save_replay_handler(int seconds, bool has_restart_replay, bool restart_replay, char *error_message, size_t error_message_size, void *userdata) { + const gsr_recorder_settings *settings = userdata; + if(!settings->is_replaying) { + snprintf(error_message, error_message_size, "option -r is required to save a replay"); + return false; + } + + int restart_replay_request = GSR_RESTART_REPLAY_USE_OPTION; + if(has_restart_replay) + restart_replay_request = restart_replay ? GSR_RESTART_REPLAY_ENABLE : GSR_RESTART_REPLAY_DISABLE; + + gsr_recorder_save_replay(recorder, seconds, restart_replay_request); + return true; +} + +static void install_signal_handlers(void) { + signal(SIGINT, stop_handler); + signal(SIGTERM, stop_handler); + signal(SIGUSR1, save_replay_handler); + signal(SIGUSR2, toggle_pause_handler); + signal(SIGRTMIN, toggle_replay_recording_handler); + signal(SIGRTMIN+1, save_replay_10_seconds_handler); + signal(SIGRTMIN+2, save_replay_30_seconds_handler); + signal(SIGRTMIN+3, save_replay_1_minute_handler); + signal(SIGRTMIN+4, save_replay_5_minutes_handler); + signal(SIGRTMIN+5, save_replay_10_minutes_handler); + signal(SIGRTMIN+6, save_replay_30_minutes_handler); +} + +static void set_display_server_environment_variables(void) { + /* Some users dont have properly setup environments (no display manager that does systemctl --user import-environment DISPLAY WAYLAND_DISPLAY) */ + const char *display = getenv("DISPLAY"); + if(!display) { + display = ":0"; + setenv("DISPLAY", display, true); + } + + const char *wayland_display = getenv("WAYLAND_DISPLAY"); + if(!wayland_display) { + wayland_display = "wayland-0"; + setenv("WAYLAND_DISPLAY", wayland_display, true); + } +} + +static void set_environment_variables(void) { + set_display_server_environment_variables(); + + /* Linux nvidia driver 580.105.08 added the environment variable CUDA_DISABLE_PERF_BOOST to disable the p2 power level issue, + where running cuda (which includes nvenc) causes the gpu to be forcefully set to p2 power level which on many nvidia gpus + decreases gpu performance in games. On my GTX 1080 it decreased game performance by 10% for absolutely no reason. */ + setenv("CUDA_DISABLE_PERF_BOOST", "1", true); + /* Stop nvidia driver from buffering frames */ + setenv("__GL_MaxFramesAllowed", "1", true); + /* If this is set to 1 then cuGraphicsGLRegisterImage will fail for egl context with error: invalid OpenGL or DirectX context, + so we overwrite it */ + setenv("__GL_THREADED_OPTIMIZATIONS", "0", true); + /* Some people set this to nvidia (for nvdec) or vdpau (for nvidia vdpau), which breaks gpu screen recorder since + nvidia doesn't support vaapi and nvidia-vaapi-driver doesn't support encoding yet. + Let vaapi find the right vaapi driver instead of forcing a specific one. */ + unsetenv("LIBVA_DRIVER_NAME"); + /* Some people set this to force all applications to vsync on nvidia, but this makes eglSwapBuffers never return. */ + unsetenv("__GL_SYNC_TO_VBLANK"); + /* Same as above, but for amd/intel */ + unsetenv("vblank_mode"); +} + +static void install_cuda_no_stable_perf_limit(void) { + if(access("/proc/driver/nvidia/version", F_OK) != 0) + return; + + const char *home = getenv("HOME"); + if(!home) { + gsr_log(GSR_LOG_LEVEL_WARNING, "install_cuda_no_stable_perf_limit: $HOME not set"); + return; + } + + char nv_profiles_path[4096]; + snprintf(nv_profiles_path, sizeof(nv_profiles_path), "%s/.nv/nvidia-application-profiles-rc.d", home); + + if(create_directory_recursive(nv_profiles_path) != 0) { + gsr_log(GSR_LOG_LEVEL_WARNING, "install_cuda_no_stable_perf_limit: failed to create directory: %s", nv_profiles_path); + return; + } + + snprintf(nv_profiles_path, sizeof(nv_profiles_path), "%s/.nv/nvidia-application-profiles-rc.d/10-gsr-cuda-no-stable-perf-limit", home); + + FILE *f = fopen(nv_profiles_path, "wb"); + if(!f) { + gsr_log(GSR_LOG_LEVEL_WARNING, "install_cuda_no_stable_perf_limit: failed to create file: %s", nv_profiles_path); + return; + } + + const char *profile_data = + "{\n" + " \"profiles\": [\n" + " {\n" + " \"name\": \"CudaNoStablePerfLimit\",\n" + " \"settings\": [\"0x166c5e\", 0]\n" + " }\n" + " ],\n" + " \"rules\": [\n" + " { \"pattern\": \"gpu-screen-recorder\", \"profile\": \"CudaNoStablePerfLimit\" }\n" + " ]\n" + "}\n"; + + fwrite(profile_data, 1, strlen(profile_data), f); + fclose(f); +} + +static int validate_args_with_capture_sources(args_parser *arg_parser, const gsr_capture_sources *capture_sources) { + const Arg *output_resolution_arg = args_parser_get_arg(arg_parser, "-s"); + assert(output_resolution_arg); + + const Arg *region_arg = args_parser_get_arg(arg_parser, "-region"); + assert(region_arg); + + if(gsr_capture_sources_has_type(capture_sources, GSR_CAPTURE_SOURCE_TYPE_FOCUSED_WINDOW) && output_resolution_arg->num_values == 0) { + gsr_log(GSR_LOG_LEVEL_ERROR, "option -s is required when using '-w focused' option"); + args_parser_print_usage(); + return GSR_ERROR_GENERIC; + } + + const bool is_capturing_region = gsr_capture_sources_has_type(capture_sources, GSR_CAPTURE_SOURCE_TYPE_REGION); + if(region_arg->num_values == 0) { + if(is_capturing_region && !gsr_capture_sources_has_region_set(capture_sources)) { + gsr_log(GSR_LOG_LEVEL_ERROR, "option -region is required when '-w region' is used"); + args_parser_print_usage(); + return GSR_ERROR_GENERIC; + } + } else { + if(is_capturing_region) { + gsr_log(GSR_LOG_LEVEL_WARNING, "option -region is deprecated, use -w with region directly instead, for example: -w %s", region_arg->values[0]); + } else { + gsr_log(GSR_LOG_LEVEL_ERROR, "option -region can only be used when option '-w region' is used"); + args_parser_print_usage(); + return GSR_ERROR_GENERIC; + } + } + + if(!arg_parser->settings.restore_portal_session && gsr_capture_sources_has_type(capture_sources, GSR_CAPTURE_SOURCE_TYPE_PORTAL)) + gsr_log(GSR_LOG_LEVEL_INFO, "option '-w portal' was used without '-restore-portal-session yes'. The previous screencast session will be ignored"); + + return GSR_ERROR_OK; +} + +static void screenshot_saved_callback(const char *filepath, void *userdata) { + const char *recording_saved_script = userdata; + if(recording_saved_script) + run_recording_saved_script_async(recording_saved_script, filepath, "screenshot"); +} + +typedef struct { + const char *recording_saved_script; + gsr_ipc *ipc; +} recorder_callbacks_context; + +static void replay_saved_callback(const char *filepath, void *userdata) { + recorder_callbacks_context *context = userdata; + if(!filepath) { + printf("gsr error: Failed to save replay\n"); + fflush(stdout); + gsr_ipc_complete_request(context->ipc, GSR_IPC_DEFERRED_REQUEST_SAVE_REPLAY, false, NULL); + return; + } + + puts(filepath); + fflush(stdout); + if(context->recording_saved_script) + run_recording_saved_script_async(context->recording_saved_script, filepath, "replay"); + + gsr_ipc_complete_request(context->ipc, GSR_IPC_DEFERRED_REQUEST_SAVE_REPLAY, true, filepath); +} + +static void recording_started_callback(const char *filepath, void *userdata) { + (void)userdata; + if(!filepath) { + printf("gsr error: Failed to start recording\n"); + fflush(stdout); + } +} + +static void recording_stopped_callback(const char *filepath, void *userdata) { + recorder_callbacks_context *context = userdata; + if(!filepath) { + printf("gsr error: Failed to save recording\n"); + fflush(stdout); + gsr_ipc_complete_request(context->ipc, GSR_IPC_DEFERRED_REQUEST_STOP_REPLAY_RECORDING, false, NULL); + return; + } + + puts(filepath); + fflush(stdout); + if(context->recording_saved_script) + run_recording_saved_script_async(context->recording_saved_script, filepath, "regular"); + + gsr_ipc_complete_request(context->ipc, GSR_IPC_DEFERRED_REQUEST_STOP_REPLAY_RECORDING, true, filepath); +} + +#ifdef GSR_APP_AUDIO +static gsr_pipewire_audio pipewire_audio; + +static bool app_audio_name_callback(const char *app_name, void *userdata) { + gsr_app_audio_names *app_audio_names = userdata; + gsr_app_audio_names_add(app_audio_names, app_name); + return true; +} + +static int setup_app_audio(gsr_app_audio_names *app_audio_names) { + if(!pulseaudio_server_is_pipewire()) { + gsr_log(GSR_LOG_LEVEL_ERROR, "your sound server is not PipeWire. Application audio is only available when running PipeWire audio server"); + return GSR_ERROR_UNSUPPORTED; + } + + if(!gsr_pipewire_audio_init(&pipewire_audio)) { + gsr_log(GSR_LOG_LEVEL_ERROR, "failed to setup PipeWire audio for application audio capture"); + return GSR_ERROR_UNSUPPORTED; + } + + gsr_pipewire_audio_for_each_app(&pipewire_audio, app_audio_name_callback, app_audio_names); + return GSR_ERROR_OK; +} +#endif + +static int parse_audio_inputs(args_parser *arg_parser, gsr_audio_input_tracks *audio_input_tracks) { + const Arg *audio_input_arg = args_parser_get_arg(arg_parser, "-a"); + assert(audio_input_arg); + + gsr_audio_devices audio_devices; + memset(&audio_devices, 0, sizeof(audio_devices)); + if(audio_input_arg->num_values > 0) + get_pulseaudio_inputs(&audio_devices); + + const int parse_result = gsr_audio_input_tracks_parse(audio_input_tracks, audio_input_arg->values, audio_input_arg->num_values, &audio_devices); + gsr_audio_devices_deinit(&audio_devices); + return parse_result; +} + +static int take_screenshot(args_parser *arg_parser, gsr_windowing *windowing, gsr_capture_deps *capture_deps, gsr_capture_sources *capture_sources, gsr_image_format image_format) { + const Arg *plugin_arg = args_parser_get_arg(arg_parser, "-p"); + assert(plugin_arg); + + arg_parser->settings.fps = 60; /* We want to capture an image as soon as possible */ + + gsr_screenshot_params screenshot_params; + memset(&screenshot_params, 0, sizeof(screenshot_params)); + screenshot_params.settings = &arg_parser->settings; + screenshot_params.egl = &windowing->egl; + screenshot_params.window = windowing->window; + screenshot_params.capture_deps = capture_deps; + screenshot_params.capture_sources = capture_sources; + screenshot_params.image_format = image_format; + screenshot_params.plugin_filepaths = plugin_arg->values; + screenshot_params.num_plugin_filepaths = plugin_arg->num_values; + screenshot_params.running = &running; + screenshot_params.screenshot_saved = screenshot_saved_callback; + screenshot_params.userdata = (void*)arg_parser->settings.recording_saved_script; + + return gsr_screenshot_take(&screenshot_params); +} + +static int record(args_parser *arg_parser, gsr_windowing *windowing, gsr_capture_deps *capture_deps, gsr_capture_sources *capture_sources, gsr_audio_input_tracks *audio_input_tracks, gsr_ipc *ipc) { + const Arg *plugin_arg = args_parser_get_arg(arg_parser, "-p"); + assert(plugin_arg); + + gsr_recorder_params recorder_params; + memset(&recorder_params, 0, sizeof(recorder_params)); + recorder_params.settings = &arg_parser->settings; + recorder_params.windowing = windowing; + recorder_params.capture_deps = capture_deps; + recorder_params.capture_sources = capture_sources; + recorder_params.audio_input_tracks = audio_input_tracks; + recorder_params.plugin_filepaths = plugin_arg->values; + recorder_params.num_plugin_filepaths = plugin_arg->num_values; +#ifdef GSR_APP_AUDIO + recorder_params.pipewire_audio = &pipewire_audio; +#endif + + recorder_callbacks_context callbacks_context; + callbacks_context.recording_saved_script = arg_parser->settings.recording_saved_script; + callbacks_context.ipc = ipc; + + gsr_recorder_callbacks callbacks; + memset(&callbacks, 0, sizeof(callbacks)); + callbacks.replay_saved = replay_saved_callback; + callbacks.recording_started = recording_started_callback; + callbacks.recording_stopped = recording_stopped_callback; + callbacks.userdata = &callbacks_context; + + int error = GSR_ERROR_OK; + recorder = gsr_recorder_create(&recorder_params, &callbacks, &error); + if(!recorder) + return error; + + apply_pending_signals(recorder); + if(!atomic_load(&running)) + gsr_recorder_stop(recorder); + + gsr_ipc_handlers ipc_handlers; + memset(&ipc_handlers, 0, sizeof(ipc_handlers)); + ipc_handlers.stop = ipc_stop_handler; + ipc_handlers.toggle_pause = ipc_toggle_pause_handler; + ipc_handlers.set_paused = ipc_set_paused_handler; + ipc_handlers.toggle_replay_recording = ipc_toggle_replay_recording_handler; + ipc_handlers.start_replay_recording = ipc_start_replay_recording_handler; + ipc_handlers.stop_replay_recording = ipc_stop_replay_recording_handler; + ipc_handlers.save_replay = ipc_save_replay_handler; + ipc_handlers.userdata = &arg_parser->settings; + + int run_result = gsr_ipc_start(ipc, &ipc_handlers); + if(run_result == GSR_ERROR_OK) + run_result = gsr_recorder_run(recorder); + + gsr_ipc_complete_request(ipc, GSR_IPC_DEFERRED_REQUEST_STOP, true, arg_parser->settings.is_replaying ? NULL : arg_parser->settings.filename); + gsr_ipc_stop(ipc); + gsr_recorder_destroy(recorder, true); + recorder = NULL; + return run_result; +} + +static int run(args_parser *arg_parser) { + int exit_code = 0; + + gsr_capture_sources capture_sources; + gsr_audio_input_tracks audio_input_tracks; + gsr_app_audio_names app_audio_names; + gsr_windowing windowing; + gsr_capture_deps capture_deps; + gsr_ipc ipc; + memset(&audio_input_tracks, 0, sizeof(audio_input_tracks)); + memset(&app_audio_names, 0, sizeof(app_audio_names)); + memset(&windowing, 0, sizeof(windowing)); + memset(&ipc, 0, sizeof(ipc)); + gsr_capture_deps_init(&capture_deps); + + const Arg *ipc_arg = args_parser_get_arg(arg_parser, "-ipc"); + assert(ipc_arg); + + const int parse_capture_sources_result = gsr_capture_sources_parse(&capture_sources, arg_parser->settings.capture_source, arg_parser->settings.region_position, arg_parser->settings.region_size); + if(parse_capture_sources_result != GSR_ERROR_OK) { + exit_code = gsr_error_to_exit_code(parse_capture_sources_result); + goto done; + } + + if(capture_sources.num_items == 0) { + gsr_log(GSR_LOG_LEVEL_ERROR, "option -w can't be empty. You need to capture video from at least one source"); + args_parser_print_usage(); + exit_code = 1; + goto done; + } + + const int validate_args_result = validate_args_with_capture_sources(arg_parser, &capture_sources); + if(validate_args_result != GSR_ERROR_OK) { + exit_code = gsr_error_to_exit_code(validate_args_result); + goto done; + } + + if(ipc_arg->num_values > 0 && gsr_ipc_init(&ipc, ipc_arg->values[0]) != GSR_ERROR_OK) { + exit_code = 1; + goto done; + } + + const int parse_audio_inputs_result = parse_audio_inputs(arg_parser, &audio_input_tracks); + if(parse_audio_inputs_result != GSR_ERROR_OK) { + exit_code = gsr_error_to_exit_code(parse_audio_inputs_result); + goto done; + } + + const bool uses_app_audio = gsr_audio_input_tracks_has_app_audio(&audio_input_tracks); +#ifdef GSR_APP_AUDIO + if(uses_app_audio) { + const int app_audio_result = setup_app_audio(&app_audio_names); + if(app_audio_result != GSR_ERROR_OK) { + exit_code = gsr_error_to_exit_code(app_audio_result); + goto done; + } + } +#else + if(uses_app_audio) { + gsr_log(GSR_LOG_LEVEL_ERROR, "application audio can't be recorded because GPU Screen Recorder is built without application audio support (-Dapp_audio option)"); + exit_code = 2; + goto done; + } +#endif + + const int validate_app_audio_result = gsr_audio_input_tracks_validate_app_audio(&audio_input_tracks, &app_audio_names); + if(validate_app_audio_result != GSR_ERROR_OK) { + exit_code = gsr_error_to_exit_code(validate_app_audio_result); + goto done; + } + + gsr_windowing_params windowing_params; + windowing_params.monitor_capture = gsr_capture_sources_has_monitor_or_region(&capture_sources); + windowing_params.gl_debug = arg_parser->settings.gl_debug; + windowing_params.listen_to_x11_events = true; + if(gsr_windowing_init(&windowing, &windowing_params) != GSR_ERROR_OK) { + exit_code = 1; + goto done; + } + + if(gsr_capture_sources_has_type(&capture_sources, GSR_CAPTURE_SOURCE_TYPE_PORTAL)) { + if(gsr_windowing_is_using_prime_run()) { + gsr_log(GSR_LOG_LEVEL_WARNING, "use of prime-run with -w portal option is currently not supported. Disabling prime-run"); + gsr_windowing_disable_prime_run(); + } + + if(video_codec_is_hdr(arg_parser->settings.video_codec)) { + gsr_log(GSR_LOG_LEVEL_WARNING, "portal capture option doesn't support hdr yet (PipeWire doesn't support hdr), the video will be tonemapped from hdr to sdr"); + arg_parser->settings.video_codec = hdr_video_codec_to_sdr_video_codec(arg_parser->settings.video_codec); + } + } + + if(gsr_windowing_load_egl(&windowing, &windowing_params) != GSR_ERROR_OK) { + exit_code = 1; + goto done; + } + + gsr_shader_enable_debug_output(arg_parser->settings.gl_debug); +#ifndef NDEBUG + gsr_shader_enable_debug_output(true); +#endif + + if(!args_parser_validate_with_gl_info(arg_parser, &windowing.egl)) { + exit_code = 1; + goto done; + } + + if(!windowing.card_path_found) { + gsr_log(GSR_LOG_LEVEL_ERROR, "no /dev/dri/cardX device found. Make sure that you have at least one monitor connected or record a single window instead on X11 or record with the -w portal option"); + exit_code = 2; + goto done; + } + + gsr_capture_deps_init_cursor(&capture_deps, &windowing.egl, arg_parser->settings.record_cursor); + + gsr_image_format image_format; + if(get_image_format_from_filename(arg_parser->settings.filename, &image_format)) { + if(audio_input_tracks.num_items > 0) { + gsr_log(GSR_LOG_LEVEL_ERROR, "can't record audio (-a) when taking a screenshot"); + exit_code = 1; + goto done; + } + + if(ipc_arg->num_values > 0) + gsr_log(GSR_LOG_LEVEL_WARNING, "option -ipc has no effect when taking a screenshot"); + + exit_code = gsr_error_to_exit_code(take_screenshot(arg_parser, &windowing, &capture_deps, &capture_sources, image_format)); + } else { + exit_code = gsr_error_to_exit_code(record(arg_parser, &windowing, &capture_deps, &capture_sources, &audio_input_tracks, &ipc)); + } + + done: + gsr_ipc_deinit(&ipc); + gsr_capture_deps_deinit(&capture_deps); + gsr_windowing_deinit(&windowing); +#ifdef GSR_APP_AUDIO + gsr_pipewire_audio_deinit(&pipewire_audio); +#endif + gsr_app_audio_names_deinit(&app_audio_names); + gsr_audio_input_tracks_deinit(&audio_input_tracks); + gsr_capture_sources_deinit(&capture_sources); + return exit_code; +} + +int main(int argc, char **argv) { + setlocale(LC_ALL, "C"); /* Sigh... stupid C */ +#ifdef __GLIBC__ + mallopt(M_MMAP_THRESHOLD, 65536); +#endif + + install_signal_handlers(); + set_environment_variables(); + install_cuda_no_stable_perf_limit(); + + if(geteuid() == 0) { + gsr_log(GSR_LOG_LEVEL_ERROR, "don't run gpu-screen-recorder as the root user"); + _exit(1); + } + + args_handlers arg_handlers; + arg_handlers.version = version_command; + arg_handlers.info = info_command; + arg_handlers.list_audio_devices = list_audio_devices_command; + arg_handlers.list_application_audio = list_application_audio_command; + arg_handlers.list_v4l2_devices = list_v4l2_devices; + arg_handlers.list_capture_options = list_capture_options_command; + arg_handlers.list_monitors = list_monitors_command; + + args_parser arg_parser; + int exit_code = 0; + int command_exit_code = 0; + switch(args_parser_parse(&arg_parser, argc, argv, &arg_handlers, NULL, &command_exit_code)) { + case ARGS_PARSE_RESULT_ERROR: + exit_code = 1; + break; + case ARGS_PARSE_RESULT_COMMAND_HANDLED: + exit_code = command_exit_code; + break; + case ARGS_PARSE_RESULT_OK: { + if(!arg_parser.settings.low_power) { + /* Forces low latency encoding mode. Use this environment variable until vaapi supports setting this as a parameter. + The downside of this is that it always uses maximum power, which is not ideal for replay mode that runs on system startup. + This option was added in mesa 24.1.4, released in july 17, 2024. + Seems like the performance issue is not in encoding, but rendering the frame. + Some frames end up taking 10 times longer. Seems to be an issue with amd gpu power management when letting the application sleep on the cpu side? */ + setenv("AMD_DEBUG", "lowlatencyenc", true); + } + + exit_code = run(&arg_parser); + break; + } + } + + args_parser_deinit(&arg_parser); + + /* We do an _exit here because cuda uses at_exit to do _something_ that causes the program to freeze, + but only on some nvidia driver versions on some gpus (RTX?), and _exit exits the program without calling + the at_exit registered functions. + Cuda (nvenc) is loaded in a separate process, but this still happens. */ + _exit(exit_code); +} diff --git a/src/codec_query/nvenc.c b/src/codec_query/nvenc.c index 37c25ba..4416b91 100644 --- a/src/codec_query/nvenc.c +++ b/src/codec_query/nvenc.c @@ -1,9 +1,9 @@ #include "../../include/codec_query/nvenc.h" +#include "../../include/log.h" #include "../../include/cuda.h" #include "../../external/nvEncodeAPI.h" #include <dlfcn.h> -#include <stdio.h> #include <string.h> #define NVENCAPI_MAJOR_VERSION_470 11 @@ -17,7 +17,7 @@ static void* open_nvenc_library(void) { if(!lib) { lib = dlopen("libnvidia-encode.so", RTLD_LAZY); if(!lib) { - fprintf(stderr, "gsr error: gsr_get_supported_video_codecs_nvenc failed: failed to load libnvidia-encode.so/libnvidia-encode.so.1, error: %s\n", dlerror()); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_get_supported_video_codecs_nvenc failed: failed to load libnvidia-encode.so/libnvidia-encode.so.1, error: %s", dlerror()); return NULL; } } @@ -107,7 +107,7 @@ static bool encoder_get_supported_profiles(const NV_ENCODE_API_FUNCTION_LIST *fu uint32_t profile_guid_count = 0; if(function_list->nvEncGetEncodeProfileGUIDCount(nvenc_encoder, *encoder_guid, &profile_guid_count) != NV_ENC_SUCCESS) { - fprintf(stderr, "gsr error: gsr_get_supported_video_codecs_nvenc: nvEncGetEncodeProfileGUIDCount failed, error: %s\n", function_list->nvEncGetLastErrorString(nvenc_encoder)); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_get_supported_video_codecs_nvenc: nvEncGetEncodeProfileGUIDCount failed, error: %s", function_list->nvEncGetLastErrorString(nvenc_encoder)); goto fail; } @@ -116,12 +116,12 @@ static bool encoder_get_supported_profiles(const NV_ENCODE_API_FUNCTION_LIST *fu profile_guids = calloc(profile_guid_count, sizeof(GUID)); if(!profile_guids) { - fprintf(stderr, "gsr error: gsr_get_supported_video_codecs_nvenc: failed to allocate %d guids\n", (int)profile_guid_count); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_get_supported_video_codecs_nvenc: failed to allocate %d guids", (int)profile_guid_count); goto fail; } if(function_list->nvEncGetEncodeProfileGUIDs(nvenc_encoder, *encoder_guid, profile_guids, profile_guid_count, &profile_guid_count) != NV_ENC_SUCCESS) { - fprintf(stderr, "gsr error: gsr_get_supported_video_codecs_nvenc: nvEncGetEncodeProfileGUIDs failed, error: %s\n", function_list->nvEncGetLastErrorString(nvenc_encoder)); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_get_supported_video_codecs_nvenc: nvEncGetEncodeProfileGUIDs failed, error: %s", function_list->nvEncGetLastErrorString(nvenc_encoder)); goto fail; } @@ -157,7 +157,7 @@ static bool get_supported_video_codecs(const NV_ENCODE_API_FUNCTION_LIST *functi uint32_t encode_guid_count = 0; if(function_list->nvEncGetEncodeGUIDCount(nvenc_encoder, &encode_guid_count) != NV_ENC_SUCCESS) { - fprintf(stderr, "gsr error: gsr_get_supported_video_codecs_nvenc: nvEncGetEncodeGUIDCount failed, error: %s\n", function_list->nvEncGetLastErrorString(nvenc_encoder)); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_get_supported_video_codecs_nvenc: nvEncGetEncodeGUIDCount failed, error: %s", function_list->nvEncGetLastErrorString(nvenc_encoder)); goto fail; } @@ -166,12 +166,12 @@ static bool get_supported_video_codecs(const NV_ENCODE_API_FUNCTION_LIST *functi encoder_guids = calloc(encode_guid_count, sizeof(GUID)); if(!encoder_guids) { - fprintf(stderr, "gsr error: gsr_get_supported_video_codecs_nvenc: failed to allocate %d guids\n", (int)encode_guid_count); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_get_supported_video_codecs_nvenc: failed to allocate %d guids", (int)encode_guid_count); goto fail; } if(function_list->nvEncGetEncodeGUIDs(nvenc_encoder, encoder_guids, encode_guid_count, &encode_guid_count) != NV_ENC_SUCCESS) { - fprintf(stderr, "gsr error: gsr_get_supported_video_codecs_nvenc: nvEncGetEncodeGUIDs failed, error: %s\n", function_list->nvEncGetLastErrorString(nvenc_encoder)); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_get_supported_video_codecs_nvenc: nvEncGetEncodeGUIDs failed, error: %s", function_list->nvEncGetLastErrorString(nvenc_encoder)); goto fail; } @@ -198,7 +198,7 @@ bool gsr_get_supported_video_codecs_nvenc(gsr_supported_video_codecs *video_code memset(&cuda, 0, sizeof(cuda)); if(!gsr_cuda_load(&cuda)) { - fprintf(stderr, "gsr error: gsr_get_supported_video_codecs_nvenc: failed to load cuda\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_get_supported_video_codecs_nvenc: failed to load cuda"); goto done; } @@ -209,7 +209,7 @@ bool gsr_get_supported_video_codecs_nvenc(gsr_supported_video_codecs *video_code typedef NVENCSTATUS NVENCAPI (*FUNC_NvEncodeAPICreateInstance)(NV_ENCODE_API_FUNCTION_LIST *functionList); FUNC_NvEncodeAPICreateInstance nvEncodeAPICreateInstance = (FUNC_NvEncodeAPICreateInstance)dlsym(nvenc_lib, "NvEncodeAPICreateInstance"); if(!nvEncodeAPICreateInstance) { - fprintf(stderr, "gsr error: gsr_get_supported_video_codecs_nvenc: failed to find NvEncodeAPICreateInstance in libnvidia-encode.so\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_get_supported_video_codecs_nvenc: failed to find NvEncodeAPICreateInstance in libnvidia-encode.so"); goto done; } @@ -217,7 +217,7 @@ bool gsr_get_supported_video_codecs_nvenc(gsr_supported_video_codecs *video_code memset(&function_list, 0, sizeof(function_list)); function_list.version = NVENCAPI_STRUCT_VERSION(2); if(nvEncodeAPICreateInstance(&function_list) != NV_ENC_SUCCESS) { - fprintf(stderr, "gsr error: gsr_get_supported_video_codecs_nvenc: nvEncodeAPICreateInstance failed\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_get_supported_video_codecs_nvenc: nvEncodeAPICreateInstance failed"); goto done; } @@ -232,14 +232,14 @@ bool gsr_get_supported_video_codecs_nvenc(gsr_supported_video_codecs *video_code // In such cases fallback to old api version if possible and try again. function_list.version = NVENCAPI_STRUCT_VERSION_CUSTOM(NVENCAPI_VERSION_470, 2); if(nvEncodeAPICreateInstance(&function_list) != NV_ENC_SUCCESS) { - fprintf(stderr, "gsr error: gsr_get_supported_video_codecs_nvenc: nvEncodeAPICreateInstance (retry) failed\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_get_supported_video_codecs_nvenc: nvEncodeAPICreateInstance (retry) failed"); goto done; } params.version = NVENCAPI_STRUCT_VERSION_CUSTOM(NVENCAPI_VERSION_470, 1); params.apiVersion = NVENCAPI_VERSION_470; if(function_list.nvEncOpenEncodeSessionEx(¶ms, &nvenc_encoder) != NV_ENC_SUCCESS) { - fprintf(stderr, "gsr error: gsr_get_supported_video_codecs_nvenc: nvEncOpenEncodeSessionEx (retry) failed\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_get_supported_video_codecs_nvenc: nvEncOpenEncodeSessionEx (retry) failed"); goto done; } } diff --git a/src/codec_query/vaapi.c b/src/codec_query/vaapi.c index 0943450..ca4b3f4 100644 --- a/src/codec_query/vaapi.c +++ b/src/codec_query/vaapi.c @@ -1,8 +1,8 @@ #include "../../include/codec_query/vaapi.h" +#include "../../include/log.h" #include "../../include/utils.h" #include <stdlib.h> -#include <stdio.h> #include <string.h> #include <fcntl.h> #include <unistd.h> @@ -134,7 +134,7 @@ static bool get_supported_video_codecs(VADisplay va_dpy, gsr_supported_video_cod int va_major = 0; int va_minor = 0; if(vaInitialize(va_dpy, &va_major, &va_minor) != VA_STATUS_SUCCESS) { - fprintf(stderr, "gsr error: gsr_get_supported_video_codecs_vaapi: vaInitialize failed\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_get_supported_video_codecs_vaapi: vaInitialize failed"); return false; } @@ -207,20 +207,20 @@ bool gsr_get_supported_video_codecs_vaapi(gsr_supported_video_codecs *video_code char render_path[128]; if(!gsr_card_path_get_render_path(card_path, render_path)) { - fprintf(stderr, "gsr error: gsr_get_supported_video_codecs_vaapi: failed to get /dev/dri/renderDXXX file from %s\n", card_path); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_get_supported_video_codecs_vaapi: failed to get /dev/dri/renderDXXX file from %s", card_path); goto done; } drm_fd = open(render_path, O_RDWR); if(drm_fd == -1) { - fprintf(stderr, "gsr error: gsr_get_supported_video_codecs_vaapi: failed to open device %s\n", render_path); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_get_supported_video_codecs_vaapi: failed to open device %s", render_path); goto done; } VADisplay va_dpy = vaGetDisplayDRM(drm_fd); if(va_dpy) { if(!get_supported_video_codecs(va_dpy, video_codecs, cleanup)) { - fprintf(stderr, "gsr error: gsr_get_supported_video_codecs_vaapi: failed to query supported video codecs for device %s\n", render_path); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_get_supported_video_codecs_vaapi: failed to query supported video codecs for device %s", render_path); goto done; } success = true; diff --git a/src/codec_query/vulkan.c b/src/codec_query/vulkan.c index 7dcb44e..d61cee5 100644 --- a/src/codec_query/vulkan.c +++ b/src/codec_query/vulkan.c @@ -1,4 +1,5 @@ #include "../../include/codec_query/vulkan.h" +#include "../../include/log.h" #include "../../include/utils.h" #include <stdio.h> @@ -162,25 +163,25 @@ bool gsr_get_supported_video_codecs_vulkan(gsr_supported_video_codecs *video_cod char render_path[128]; if(!gsr_card_path_get_render_path(card_path, render_path)) { - fprintf(stderr, "gsr error: gsr_get_supported_video_codecs_vulkan: failed to get /dev/dri/renderDXXX file from %s\n", card_path); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_get_supported_video_codecs_vulkan: failed to get /dev/dri/renderDXXX file from %s", card_path); return false; } libvulkan = dlopen("libvulkan.so.1", RTLD_NOW); if (!libvulkan) { - fprintf(stderr, "gsr error: gsr_get_supported_video_codecs_vulkan: failed to load libvulkan.so.1, error: %s\n", dlerror()); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_get_supported_video_codecs_vulkan: failed to load libvulkan.so.1, error: %s", dlerror()); return false; } PFN_vkGetInstanceProcAddr vkGetInstanceProcAddr = (PFN_vkGetInstanceProcAddr)dlsym(libvulkan, "vkGetInstanceProcAddr"); if (!vkGetInstanceProcAddr) { - fprintf(stderr, "gsr error: gsr_get_supported_video_codecs_vulkan: could not find vkGetInstanceProcAddr in libvulkan.so.1\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_get_supported_video_codecs_vulkan: could not find vkGetInstanceProcAddr in libvulkan.so.1"); goto done; } PFN_vkCreateInstance vkCreateInstance = (PFN_vkCreateInstance)vkGetInstanceProcAddr(NULL, "vkCreateInstance"); if(!vkCreateInstance) { - fprintf(stderr, "gsr error: gsr_get_supported_video_codecs_vulkan: could not find vkCreateInstance in libvulkan.so.1\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_get_supported_video_codecs_vulkan: could not find vkCreateInstance in libvulkan.so.1"); goto done; } @@ -199,7 +200,7 @@ bool gsr_get_supported_video_codecs_vulkan(gsr_supported_video_codecs *video_cod }; if(vkCreateInstance(&instance_create_info, NULL, &instance) != VK_SUCCESS) { - fprintf(stderr, "gsr error: gsr_get_supported_video_codecs_vulkan: vkCreateInstance failed\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_get_supported_video_codecs_vulkan: vkCreateInstance failed"); goto done; } @@ -211,7 +212,7 @@ bool gsr_get_supported_video_codecs_vulkan(gsr_supported_video_codecs *video_cod PFN_vkDestroyDevice vkDestroyDevice = NULL; PFN_vkGetPhysicalDeviceVideoCapabilitiesKHR vkGetPhysicalDeviceVideoCapabilitiesKHR = NULL; - #define LOAD_INST(name) name = (PFN_##name)vkGetInstanceProcAddr(instance, #name); if(!name) { fprintf(stderr, "gsr error: gsr_get_supported_video_codecs_vulkan: could not find " #name " in libvulkan.so.1\n"); goto done; } + #define LOAD_INST(name) name = (PFN_##name)vkGetInstanceProcAddr(instance, #name); if(!name) { gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_get_supported_video_codecs_vulkan: could not find " #name " in libvulkan.so.1"); goto done; } LOAD_INST(vkEnumeratePhysicalDevices) LOAD_INST(vkDestroyInstance) @@ -225,12 +226,12 @@ bool gsr_get_supported_video_codecs_vulkan(gsr_supported_video_codecs *video_cod uint32_t num_devices = 0; if(vkEnumeratePhysicalDevices(instance, &num_devices, NULL) != VK_SUCCESS) { - fprintf(stderr, "gsr error: gsr_get_supported_video_codecs_vulkan: vkEnumeratePhysicalDevices (query num devices) failed\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_get_supported_video_codecs_vulkan: vkEnumeratePhysicalDevices (query num devices) failed"); goto done; } if(num_devices == 0) { - fprintf(stderr, "gsr error: gsr_get_supported_video_codecs_vulkan: no vulkan capable device found\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_get_supported_video_codecs_vulkan: no vulkan capable device found"); goto done; } @@ -238,7 +239,7 @@ bool gsr_get_supported_video_codecs_vulkan(gsr_supported_video_codecs *video_cod num_devices = MAX_PHYSICAL_DEVICES; if(vkEnumeratePhysicalDevices(instance, &num_devices, physical_devices) != VK_SUCCESS) { - fprintf(stderr, "gsr error: gsr_get_supported_video_codecs_vulkan: vkEnumeratePhysicalDevices (get data) failed\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_get_supported_video_codecs_vulkan: vkEnumeratePhysicalDevices (get data) failed"); goto done; } @@ -267,7 +268,7 @@ bool gsr_get_supported_video_codecs_vulkan(gsr_supported_video_codecs *video_cod } if(!physical_device) { - fprintf(stderr, "gsr error: gsr_get_supported_video_codecs_vulkan: failed to find a vulkan device that matches opengl device %s\n", card_path); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_get_supported_video_codecs_vulkan: failed to find a vulkan device that matches opengl device %s", card_path); goto done; } @@ -284,18 +285,18 @@ bool gsr_get_supported_video_codecs_vulkan(gsr_supported_video_codecs *video_cod uint32_t num_device_extensions = 0; if(vkEnumerateDeviceExtensionProperties(physical_device, NULL, &num_device_extensions, NULL) != VK_SUCCESS) { - fprintf(stderr, "gsr error: gsr_get_supported_video_codecs_vulkan: vkEnumerateDeviceExtensionProperties (query num device extensions) failed\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_get_supported_video_codecs_vulkan: vkEnumerateDeviceExtensionProperties (query num device extensions) failed"); goto done; } device_extensions = calloc(num_device_extensions, sizeof(VkExtensionProperties)); if(!device_extensions) { - fprintf(stderr, "gsr error: gsr_get_supported_video_codecs_vulkan: failed to allocate %d device extensions\n", num_device_extensions); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_get_supported_video_codecs_vulkan: failed to allocate %d device extensions", num_device_extensions); goto done; } if(vkEnumerateDeviceExtensionProperties(physical_device, NULL, &num_device_extensions, device_extensions) != VK_SUCCESS) { - fprintf(stderr, "gsr error: gsr_get_supported_video_codecs_vulkan: vkEnumerateDeviceExtensionProperties (get data) failed\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_get_supported_video_codecs_vulkan: vkEnumerateDeviceExtensionProperties (get data) failed"); goto done; } diff --git a/src/color_conversion.c b/src/color_conversion.c index 8076c2b..da922cd 100644 --- a/src/color_conversion.c +++ b/src/color_conversion.c @@ -1,4 +1,5 @@ #include "../include/color_conversion.h" +#include "../include/log.h" #include "../include/egl.h" #include <stdio.h> #include <string.h> @@ -620,23 +621,23 @@ static bool gsr_color_conversion_load_hdr_graphics_shaders(gsr_color_conversion case GSR_DESTINATION_COLOR_NV12: case GSR_DESTINATION_COLOR_P010: { if(load_graphics_shader_y(&self->graphics_shaders[GRAPHICS_SHADER_INDEX_Y_HDR], self->params.egl, &self->graphics_uniforms[GRAPHICS_SHADER_INDEX_Y_HDR], self->params.destination_color, self->params.color_range, false, true) != 0) { - fprintf(stderr, "gsr error: gsr_color_conversion_load_hdr_graphics_shaders: failed to load Y graphics shader (hdr)\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_color_conversion_load_hdr_graphics_shaders: failed to load Y graphics shader (hdr)"); return false; } if(load_graphics_shader_uv(&self->graphics_shaders[GRAPHICS_SHADER_INDEX_UV_HDR], self->params.egl, &self->graphics_uniforms[GRAPHICS_SHADER_INDEX_UV_HDR], self->params.destination_color, self->params.color_range, false, true) != 0) { - fprintf(stderr, "gsr error: gsr_color_conversion_load_hdr_graphics_shaders: failed to load UV graphics shader (hdr)\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_color_conversion_load_hdr_graphics_shaders: failed to load UV graphics shader (hdr)"); return false; } if(self->params.load_external_image_shader) { if(load_graphics_shader_y(&self->graphics_shaders[GRAPHICS_SHADER_INDEX_Y_HDR_EXTERNAL], self->params.egl, &self->graphics_uniforms[GRAPHICS_SHADER_INDEX_Y_HDR_EXTERNAL], self->params.destination_color, self->params.color_range, true, true) != 0) { - fprintf(stderr, "gsr error: gsr_color_conversion_load_hdr_graphics_shaders: failed to load Y graphics shader (hdr, external)\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_color_conversion_load_hdr_graphics_shaders: failed to load Y graphics shader (hdr, external)"); return false; } if(load_graphics_shader_uv(&self->graphics_shaders[GRAPHICS_SHADER_INDEX_UV_HDR_EXTERNAL], self->params.egl, &self->graphics_uniforms[GRAPHICS_SHADER_INDEX_UV_HDR_EXTERNAL], self->params.destination_color, self->params.color_range, true, true) != 0) { - fprintf(stderr, "gsr error: gsr_color_conversion_load_hdr_graphics_shaders: failed to load UV graphics shader (hdr, external)\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_color_conversion_load_hdr_graphics_shaders: failed to load UV graphics shader (hdr, external)"); return false; } } @@ -644,13 +645,13 @@ static bool gsr_color_conversion_load_hdr_graphics_shaders(gsr_color_conversion } case GSR_DESTINATION_COLOR_RGB: { if(load_graphics_shader_rgb(&self->graphics_shaders[GRAPHICS_SHADER_INDEX_RGB_HDR], self->params.egl, &self->graphics_uniforms[GRAPHICS_SHADER_INDEX_RGB_HDR], false, true) != 0) { - fprintf(stderr, "gsr error: gsr_color_conversion_load_hdr_graphics_shaders: failed to load RGB graphics shader (hdr)\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_color_conversion_load_hdr_graphics_shaders: failed to load RGB graphics shader (hdr)"); return false; } if(self->params.load_external_image_shader) { if(load_graphics_shader_rgb(&self->graphics_shaders[GRAPHICS_SHADER_INDEX_RGB_HDR_EXTERNAL], self->params.egl, &self->graphics_uniforms[GRAPHICS_SHADER_INDEX_RGB_HDR_EXTERNAL], true, true) != 0) { - fprintf(stderr, "gsr error: gsr_color_conversion_load_hdr_graphics_shaders: failed to load RGB graphics shader (hdr, external)\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_color_conversion_load_hdr_graphics_shaders: failed to load RGB graphics shader (hdr, external)"); return false; } } @@ -686,7 +687,7 @@ static int load_framebuffers(gsr_color_conversion *self) { self->params.egl->glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, self->params.destination_textures[0], 0); self->params.egl->glDrawBuffers(1, &draw_buffer); if(self->params.egl->glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) { - fprintf(stderr, "gsr error: gsr_color_conversion_init: failed to create framebuffer for Y\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_color_conversion_init: failed to create framebuffer for Y"); goto err; } @@ -695,7 +696,7 @@ static int load_framebuffers(gsr_color_conversion *self) { self->params.egl->glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, self->params.destination_textures[1], 0); self->params.egl->glDrawBuffers(1, &draw_buffer); if(self->params.egl->glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) { - fprintf(stderr, "gsr error: gsr_color_conversion_init: failed to create framebuffer for UV\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_color_conversion_init: failed to create framebuffer for UV"); goto err; } } @@ -731,34 +732,34 @@ static bool gsr_color_conversion_load_graphics_shaders(gsr_color_conversion *sel case GSR_DESTINATION_COLOR_NV12: case GSR_DESTINATION_COLOR_P010: { if(load_graphics_shader_y(&self->graphics_shaders[GRAPHICS_SHADER_INDEX_Y], self->params.egl, &self->graphics_uniforms[GRAPHICS_SHADER_INDEX_Y], self->params.destination_color, self->params.color_range, false, false) != 0) { - fprintf(stderr, "gsr error: gsr_color_conversion_init: failed to load Y graphics shader\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_color_conversion_init: failed to load Y graphics shader"); return false; } if(load_graphics_shader_uv(&self->graphics_shaders[GRAPHICS_SHADER_INDEX_UV], self->params.egl, &self->graphics_uniforms[GRAPHICS_SHADER_INDEX_UV], self->params.destination_color, self->params.color_range, false, false) != 0) { - fprintf(stderr, "gsr error: gsr_color_conversion_init: failed to load UV graphics shader\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_color_conversion_init: failed to load UV graphics shader"); return false; } if(load_graphics_shader_yuyv_to_y(&self->graphics_shaders[GRAPHICS_SHADER_INDEX_YUYV_TO_Y], self->params.egl, &self->graphics_uniforms[GRAPHICS_SHADER_INDEX_YUYV_TO_Y], false) != 0) { - fprintf(stderr, "gsr error: gsr_color_conversion_init: failed to load YUYV to Y graphics shader\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_color_conversion_init: failed to load YUYV to Y graphics shader"); return false; } if(load_graphics_shader_yuyv_to_uv(&self->graphics_shaders[GRAPHICS_SHADER_INDEX_YUYV_TO_UV], self->params.egl, &self->graphics_uniforms[GRAPHICS_SHADER_INDEX_YUYV_TO_UV], false) != 0) { - fprintf(stderr, "gsr error: gsr_color_conversion_init: failed to load YUYV to UV graphics shader\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_color_conversion_init: failed to load YUYV to UV graphics shader"); return false; } break; } case GSR_DESTINATION_COLOR_RGB: { if(load_graphics_shader_rgb(&self->graphics_shaders[GRAPHICS_SHADER_INDEX_RGB], self->params.egl, &self->graphics_uniforms[GRAPHICS_SHADER_INDEX_RGB], false, false) != 0) { - fprintf(stderr, "gsr error: gsr_color_conversion_init: failed to load RGB graphics shader\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_color_conversion_init: failed to load RGB graphics shader"); return false; } if(load_graphics_shader_yuyv_to_rgb(&self->graphics_shaders[GRAPHICS_SHADER_INDEX_YUYV_TO_RGB], self->params.egl, &self->graphics_uniforms[GRAPHICS_SHADER_INDEX_YUYV_TO_RGB], false) != 0) { - fprintf(stderr, "gsr error: gsr_color_conversion_init: failed to load YUYV to RGB graphics shader\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_color_conversion_init: failed to load YUYV to RGB graphics shader"); return false; } break; @@ -772,34 +773,34 @@ static bool gsr_color_conversion_load_external_graphics_shaders(gsr_color_conver case GSR_DESTINATION_COLOR_NV12: case GSR_DESTINATION_COLOR_P010: { if(load_graphics_shader_y(&self->graphics_shaders[GRAPHICS_SHADER_INDEX_Y_EXTERNAL], self->params.egl, &self->graphics_uniforms[GRAPHICS_SHADER_INDEX_Y_EXTERNAL], self->params.destination_color, self->params.color_range, true, false) != 0) { - fprintf(stderr, "gsr error: gsr_color_conversion_init: failed to load Y graphics shader (external)\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_color_conversion_init: failed to load Y graphics shader (external)"); return false; } if(load_graphics_shader_uv(&self->graphics_shaders[GRAPHICS_SHADER_INDEX_UV_EXTERNAL], self->params.egl, &self->graphics_uniforms[GRAPHICS_SHADER_INDEX_UV_EXTERNAL], self->params.destination_color, self->params.color_range, true, false) != 0) { - fprintf(stderr, "gsr error: gsr_color_conversion_init: failed to load UV graphics shader (external)\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_color_conversion_init: failed to load UV graphics shader (external)"); return false; } if(load_graphics_shader_yuyv_to_y(&self->graphics_shaders[GRAPHICS_SHADER_INDEX_YUYV_TO_Y_EXTERNAL], self->params.egl, &self->graphics_uniforms[GRAPHICS_SHADER_INDEX_YUYV_TO_Y_EXTERNAL], true) != 0) { - fprintf(stderr, "gsr error: gsr_color_conversion_init: failed to load YUYV to Y graphics shader (external)\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_color_conversion_init: failed to load YUYV to Y graphics shader (external)"); return false; } if(load_graphics_shader_yuyv_to_uv(&self->graphics_shaders[GRAPHICS_SHADER_INDEX_YUYV_TO_UV_EXTERNAL], self->params.egl, &self->graphics_uniforms[GRAPHICS_SHADER_INDEX_YUYV_TO_UV_EXTERNAL], true) != 0) { - fprintf(stderr, "gsr error: gsr_color_conversion_init: failed to load YUYV to UV graphics shader (external)\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_color_conversion_init: failed to load YUYV to UV graphics shader (external)"); return false; } break; } case GSR_DESTINATION_COLOR_RGB: { if(load_graphics_shader_rgb(&self->graphics_shaders[GRAPHICS_SHADER_INDEX_RGB_EXTERNAL], self->params.egl, &self->graphics_uniforms[GRAPHICS_SHADER_INDEX_RGB_EXTERNAL], true, false) != 0) { - fprintf(stderr, "gsr error: gsr_color_conversion_init: failed to load RGB graphics shader (external)\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_color_conversion_init: failed to load RGB graphics shader (external)"); return false; } if(load_graphics_shader_yuyv_to_rgb(&self->graphics_shaders[GRAPHICS_SHADER_INDEX_YUYV_TO_RGB_EXTERNAL], self->params.egl, &self->graphics_uniforms[GRAPHICS_SHADER_INDEX_YUYV_TO_RGB_EXTERNAL], true) != 0) { - fprintf(stderr, "gsr error: gsr_color_conversion_init: failed to load YUYV to RGB graphics shader (external)\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_color_conversion_init: failed to load YUYV to RGB graphics shader (external)"); return false; } break; @@ -819,14 +820,14 @@ int gsr_color_conversion_init(gsr_color_conversion *self, const gsr_color_conver case GSR_DESTINATION_COLOR_NV12: case GSR_DESTINATION_COLOR_P010: { if(self->params.num_destination_textures != 2) { - fprintf(stderr, "gsr error: gsr_color_conversion_init: expected 2 destination textures for destination color NV12/P010, got %d destination texture(s)\n", self->params.num_destination_textures); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_color_conversion_init: expected 2 destination textures for destination color NV12/P010, got %d destination texture(s)", self->params.num_destination_textures); goto err; } break; } case GSR_DESTINATION_COLOR_RGB: { if(self->params.num_destination_textures != 1) { - fprintf(stderr, "gsr error: gsr_color_conversion_init: expected 1 destination textures for destination color RGB8, got %d destination texture(s)\n", self->params.num_destination_textures); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_color_conversion_init: expected 1 destination textures for destination color RGB8, got %d destination texture(s)", self->params.num_destination_textures); goto err; } break; @@ -1143,7 +1144,7 @@ static void gsr_color_conversion_draw_graphics(gsr_color_conversion *self, unsig void gsr_color_conversion_draw(gsr_color_conversion *self, unsigned int texture_id, vec2i destination_pos, vec2i destination_size, vec2i source_pos, vec2i source_size, vec2i texture_size, gsr_rotation rotation, gsr_flip flip, gsr_source_color source_color, bool external_texture) { assert(!external_texture || self->params.load_external_image_shader); if(external_texture && !self->params.load_external_image_shader) { - fprintf(stderr, "gsr error: gsr_color_conversion_draw: external texture not loaded\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_color_conversion_draw: external texture not loaded"); return; } @@ -1,9 +1,8 @@ #include "../include/cuda.h" +#include "../include/log.h" #include "../include/library_loader.h" #include <string.h> -#include <stdio.h> #include <dlfcn.h> -#include <assert.h> bool gsr_cuda_load(gsr_cuda *self) { memset(self, 0, sizeof(gsr_cuda)); @@ -13,7 +12,7 @@ bool gsr_cuda_load(gsr_cuda *self) { if(!lib) { lib = dlopen("libcuda.so", RTLD_LAZY); if(!lib) { - fprintf(stderr, "gsr error: gsr_cuda_load failed: failed to load libcuda.so/libcuda.so.1, error: %s\n", dlerror()); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_cuda_load failed: failed to load libcuda.so/libcuda.so.1, error: %s", dlerror()); return false; } } @@ -45,7 +44,7 @@ bool gsr_cuda_load(gsr_cuda *self) { CUresult res; if(!dlsym_load_list(lib, required_dlsym)) { - fprintf(stderr, "gsr error: gsr_cuda_load failed: missing required symbols in libcuda.so/libcuda.so.1\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_cuda_load failed: missing required symbols in libcuda.so/libcuda.so.1"); goto fail; } @@ -53,14 +52,14 @@ bool gsr_cuda_load(gsr_cuda *self) { if(res != CUDA_SUCCESS) { const char *err_str = "unknown"; self->cuGetErrorString(res, &err_str); - fprintf(stderr, "gsr error: gsr_cuda_load failed: cuInit failed, error: %s (result: %d)\n", err_str, res); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_cuda_load failed: cuInit failed, error: %s (result: %d)", err_str, res); goto fail; } int nGpu = 0; self->cuDeviceGetCount(&nGpu); if(nGpu <= 0) { - fprintf(stderr, "gsr error: gsr_cuda_load failed: no cuda supported devices found\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_cuda_load failed: no cuda supported devices found"); goto fail; } @@ -70,7 +69,7 @@ bool gsr_cuda_load(gsr_cuda *self) { if(res != CUDA_SUCCESS) { const char *err_str = "unknown"; self->cuGetErrorString(res, &err_str); - fprintf(stderr, "gsr error: gsr_cuda_load failed: unable to get CUDA device, error: %s (result: %d)\n", err_str, res); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_cuda_load failed: unable to get CUDA device, error: %s (result: %d)", err_str, res); goto fail; } @@ -78,7 +77,7 @@ bool gsr_cuda_load(gsr_cuda *self) { if(res != CUDA_SUCCESS) { const char *err_str = "unknown"; self->cuGetErrorString(res, &err_str); - fprintf(stderr, "gsr error: gsr_cuda_load failed: unable to create CUDA context, error: %s (result: %d)\n", err_str, res); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_cuda_load failed: unable to create CUDA context, error: %s (result: %d)", err_str, res); goto fail; } diff --git a/src/cursor.c b/src/cursor.c index 19047ba..e8595aa 100644 --- a/src/cursor.c +++ b/src/cursor.c @@ -1,6 +1,6 @@ #include "../include/cursor.h" +#include "../include/log.h" -#include <stdio.h> #include <stdlib.h> #include <string.h> #include <assert.h> @@ -81,7 +81,7 @@ int gsr_cursor_init(gsr_cursor *self, gsr_egl *egl, Display *display) { self->x_fixes_event_base = 0; if(!XFixesQueryExtension(self->display, &self->x_fixes_event_base, &x_fixes_error_base)) { - fprintf(stderr, "gsr error: gsr_cursor_init: your X11 server is missing the XFixes extension\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_cursor_init: your X11 server is missing the XFixes extension"); gsr_cursor_deinit(self); return -1; } diff --git a/src/damage.c b/src/damage.c index bbc025a..cc122cc 100644 --- a/src/damage.c +++ b/src/damage.c @@ -1,8 +1,8 @@ #include "../include/damage.h" +#include "../include/log.h" #include "../include/utils.h" #include "../include/window/window.h" -#include <stdio.h> #include <string.h> #include <stdlib.h> #include <X11/extensions/Xdamage.h> @@ -46,13 +46,13 @@ static void add_monitor_callback(const gsr_monitor *monitor, void *userdata) { } if(self->num_monitors + 1 > GSR_DAMAGE_MAX_MONITORS) { - fprintf(stderr, "gsr error: gsr_damage_on_output_change: max monitors reached\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_damage_on_output_change: max monitors reached"); return; } char *monitor_name_copy = strdup(monitor->name); if(!monitor_name_copy) { - fprintf(stderr, "gsr error: gsr_damage_on_output_change: strdup failed for monitor: %s\n", monitor->name); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_damage_on_output_change: strdup failed for monitor: %s", monitor->name); return; } @@ -71,25 +71,25 @@ bool gsr_damage_init(gsr_damage *self, gsr_egl *egl, gsr_cursor *cursor, bool tr self->cursor = cursor; if(gsr_window_get_display_server(egl->window) != GSR_DISPLAY_SERVER_X11) { - fprintf(stderr, "gsr error: gsr_damage_init: damage tracking is not supported on wayland\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_damage_init: damage tracking is not supported on wayland"); return false; } self->display = gsr_window_get_display(egl->window); if(!XDamageQueryExtension(self->display, &self->damage_event, &self->damage_error)) { - fprintf(stderr, "gsr error: gsr_damage_init: XDamage is not supported by your X11 server\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_damage_init: XDamage is not supported by your X11 server"); gsr_damage_deinit(self); return false; } if(!XRRQueryExtension(self->display, &self->randr_event, &self->randr_error)) { - fprintf(stderr, "gsr error: gsr_damage_init: XRandr is not supported by your X11 server\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_damage_init: XRandr is not supported by your X11 server"); gsr_damage_deinit(self); return false; } if(!xrandr_is_supported(self->display)) { - fprintf(stderr, "gsr error: gsr_damage_init: your X11 randr version is too old\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_damage_init: your X11 randr version is too old"); gsr_damage_deinit(self); return false; } @@ -98,7 +98,7 @@ bool gsr_damage_init(gsr_damage *self, gsr_egl *egl, gsr_cursor *cursor, bool tr self->monitor_damage = XDamageCreate(self->display, DefaultRootWindow(self->display), XDamageReportNonEmpty); if(!self->monitor_damage) { - fprintf(stderr, "gsr error: gsr_damage_init: XDamageCreate failed\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_damage_init: XDamageCreate failed"); gsr_damage_deinit(self); return false; } @@ -163,7 +163,7 @@ bool gsr_damage_start_tracking_window(gsr_damage *self, int64_t window) { } if(self->num_windows_tracked + 1 > GSR_DAMAGE_MAX_TRACKED_TARGETS) { - fprintf(stderr, "gsr error: gsr_damage_start_tracking_window: max window targets reached\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_damage_start_tracking_window: max window targets reached"); return false; } @@ -173,11 +173,11 @@ bool gsr_damage_start_tracking_window(gsr_damage *self, int64_t window) { win_attr.width = 0; win_attr.height = 0; if(!XGetWindowAttributes(self->display, window, &win_attr)) - fprintf(stderr, "gsr warning: gsr_damage_start_tracking_window failed: failed to get window attributes: %ld\n", (long)window); + gsr_log(GSR_LOG_LEVEL_WARNING, "gsr_damage_start_tracking_window failed: failed to get window attributes: %ld", (long)window); const Damage damage = XDamageCreate(self->display, window, XDamageReportNonEmpty); if(!damage) { - fprintf(stderr, "gsr error: gsr_damage_start_tracking_window: XDamageCreate failed\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_damage_start_tracking_window: XDamageCreate failed"); return false; } XDamageSubtract(self->display, damage, None, None); @@ -250,19 +250,19 @@ bool gsr_damage_start_tracking_monitor(gsr_damage *self, const char *monitor_nam } if(self->num_monitors_tracked + 1 > GSR_DAMAGE_MAX_TRACKED_TARGETS) { - fprintf(stderr, "gsr error: gsr_damage_start_tracking_monitor: max monitor targets reached\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_damage_start_tracking_monitor: max monitor targets reached"); return false; } char *monitor_name_copy = strdup(monitor_name); if(!monitor_name_copy) { - fprintf(stderr, "gsr error: gsr_damage_start_tracking_monitor: strdup failed for monitor: %s\n", monitor_name); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_damage_start_tracking_monitor: strdup failed for monitor: %s", monitor_name); return false; } gsr_monitor *monitor = gsr_damage_get_monitor_by_name(self, monitor_name); if(!monitor) { - fprintf(stderr, "gsr error: gsr_damage_start_tracking_monitor: failed to find monitor: %s\n", monitor_name); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_damage_start_tracking_monitor: failed to find monitor: %s", monitor_name); free(monitor_name_copy); return false; } @@ -1,4 +1,5 @@ #include "../include/dbus.h" +#include "../include/log.h" #include <sys/random.h> @@ -32,7 +33,7 @@ typedef struct { static bool generate_random_characters(char *buffer, int buffer_size, const char *alphabet, size_t alphabet_size) { /* TODO: Use other functions on other platforms than linux */ if(getrandom(buffer, buffer_size, 0) < buffer_size) { - fprintf(stderr, "Failed to get random bytes, error: %s\n", strerror(errno)); + gsr_log(GSR_LOG_LEVEL_ERROR, "failed to get random bytes, error: %s", strerror(errno)); return false; } @@ -63,25 +64,25 @@ bool gsr_dbus_init(gsr_dbus *self, const char *screencast_restore_token) { self->random_str[DBUS_RANDOM_STR_SIZE] = '\0'; if(!generate_random_characters_standard_alphabet(self->random_str, DBUS_RANDOM_STR_SIZE)) { - fprintf(stderr, "gsr error: gsr_dbus_init: failed to generate random string\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_dbus_init: failed to generate random string"); return false; } self->con = dbus_bus_get(DBUS_BUS_SESSION, &self->err); if(dbus_error_is_set(&self->err)) { - fprintf(stderr, "gsr error: gsr_dbus_init: dbus_bus_get failed with error: %s\n", self->err.message); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_dbus_init: dbus_bus_get failed with error: %s", self->err.message); return false; } if(!self->con) { - fprintf(stderr, "gsr error: gsr_dbus_init: failed to get dbus session\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_dbus_init: failed to get dbus session"); return false; } if(screencast_restore_token) { self->screencast_restore_token = strdup(screencast_restore_token); if(!self->screencast_restore_token) { - fprintf(stderr, "gsr error: gsr_dbus_init: failed to clone restore token\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_dbus_init: failed to clone restore token"); gsr_dbus_deinit(self); return false; } @@ -121,7 +122,7 @@ static bool gsr_dbus_desktop_portal_get_property(gsr_dbus *self, const char *int "org.freedesktop.DBus.Properties", // interface to call on "Get"); // method name if(!msg) { - fprintf(stderr, "gsr error: gsr_dbus_desktop_portal_get_property: dbus_message_new_method_call failed\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_dbus_desktop_portal_get_property: dbus_message_new_method_call failed"); return false; } @@ -129,20 +130,20 @@ static bool gsr_dbus_desktop_portal_get_property(gsr_dbus *self, const char *int dbus_message_iter_init_append(msg, &it); if(!dbus_message_iter_append_basic(&it, DBUS_TYPE_STRING, &interface)) { - fprintf(stderr, "gsr error: gsr_dbus_desktop_portal_get_property: failed to add interface\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_dbus_desktop_portal_get_property: failed to add interface"); dbus_message_unref(msg); return false; } if(!dbus_message_iter_append_basic(&it, DBUS_TYPE_STRING, &property_name)) { - fprintf(stderr, "gsr error: gsr_dbus_desktop_portal_get_property: failed to add property_name\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_dbus_desktop_portal_get_property: failed to add property_name"); dbus_message_unref(msg); return false; } DBusPendingCall *pending = NULL; if(!dbus_connection_send_with_reply(self->con, msg, &pending, -1) || !pending) { // -1 is default timeout - fprintf(stderr, "gsr error: gsr_dbus_desktop_portal_get_property: dbus_connection_send_with_reply failed\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_dbus_desktop_portal_get_property: dbus_connection_send_with_reply failed"); dbus_message_unref(msg); return false; } @@ -157,7 +158,7 @@ static bool gsr_dbus_desktop_portal_get_property(gsr_dbus *self, const char *int msg = dbus_pending_call_steal_reply(pending); if(!msg) { - fprintf(stderr, "gsr error: gsr_dbus_desktop_portal_get_property: dbus_pending_call_steal_reply failed\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_dbus_desktop_portal_get_property: dbus_pending_call_steal_reply failed"); dbus_pending_call_unref(pending); dbus_message_unref(msg); return false; @@ -168,7 +169,7 @@ static bool gsr_dbus_desktop_portal_get_property(gsr_dbus *self, const char *int DBusMessageIter resp_args; if(!dbus_message_iter_init(msg, &resp_args)) { - fprintf(stderr, "gsr error: gsr_dbus_desktop_portal_get_property: response message is missing arguments\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_dbus_desktop_portal_get_property: response message is missing arguments"); dbus_message_unref(msg); return false; } else if(DBUS_TYPE_UINT32 == dbus_message_iter_get_arg_type(&resp_args)) { @@ -180,12 +181,12 @@ static bool gsr_dbus_desktop_portal_get_property(gsr_dbus *self, const char *int if(dbus_message_iter_get_arg_type(&variant_iter) == DBUS_TYPE_UINT32) { dbus_message_iter_get_basic(&variant_iter, result); } else { - fprintf(stderr, "gsr error: gsr_dbus_desktop_portal_get_property: response message is not a variant with an uint32, %c\n", dbus_message_iter_get_arg_type(&variant_iter)); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_dbus_desktop_portal_get_property: response message is not a variant with an uint32, %c", dbus_message_iter_get_arg_type(&variant_iter)); dbus_message_unref(msg); return false; } } else { - fprintf(stderr, "gsr error: gsr_dbus_desktop_portal_get_property: response message is not an uint32, %c\n", dbus_message_iter_get_arg_type(&resp_args)); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_dbus_desktop_portal_get_property: response message is not an uint32, %c", dbus_message_iter_get_arg_type(&resp_args)); dbus_message_unref(msg); return false; // TODO: Check dbus_error_is_set? @@ -208,7 +209,7 @@ static bool gsr_dbus_ensure_desktop_portal_rule_added(gsr_dbus *self) { dbus_bus_add_match(self->con, DESKTOP_PORTAL_SIGNAL_RULE, &self->err); dbus_connection_flush(self->con); if(dbus_error_is_set(&self->err)) { - fprintf(stderr, "gsr error: gsr_dbus_ensure_desktop_portal_rule_added: failed to add dbus rule %s, error: %s\n", DESKTOP_PORTAL_SIGNAL_RULE, self->err.message); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_dbus_ensure_desktop_portal_rule_added: failed to add dbus rule %s, error: %s", DESKTOP_PORTAL_SIGNAL_RULE, self->err.message); return false; } self->desktop_portal_rule_added = true; @@ -293,7 +294,7 @@ static bool gsr_dbus_call_screencast_method(gsr_dbus *self, const char *method_n "org.freedesktop.portal.ScreenCast", // interface to call on method_name); // method name if(!msg) { - fprintf(stderr, "gsr error: gsr_dbus_call_screencast_method: dbus_message_new_method_call failed\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_dbus_call_screencast_method: dbus_message_new_method_call failed"); return false; } @@ -302,7 +303,7 @@ static bool gsr_dbus_call_screencast_method(gsr_dbus *self, const char *method_n if(session_handle) { if(!dbus_message_iter_append_basic(&it, DBUS_TYPE_OBJECT_PATH, &session_handle)) { - fprintf(stderr, "gsr error: gsr_dbus_call_screencast_method: failed to add session_handle\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_dbus_call_screencast_method: failed to add session_handle"); dbus_message_unref(msg); return false; } @@ -310,21 +311,21 @@ static bool gsr_dbus_call_screencast_method(gsr_dbus *self, const char *method_n if(parent_window) { if(!dbus_message_iter_append_basic(&it, DBUS_TYPE_STRING, &parent_window)) { - fprintf(stderr, "gsr error: gsr_dbus_call_screencast_method: failed to add parent_window\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_dbus_call_screencast_method: failed to add parent_window"); dbus_message_unref(msg); return false; } } if(!dbus_add_dict(&it, entries, num_entries)) { - fprintf(stderr, "gsr error: gsr_dbus_call_screencast_method: failed to add dict\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_dbus_call_screencast_method: failed to add dict"); dbus_message_unref(msg); return false; } DBusPendingCall *pending = NULL; if(!dbus_connection_send_with_reply(self->con, msg, &pending, -1) || !pending) { // -1 is default timeout - fprintf(stderr, "gsr error: gsr_dbus_call_screencast_method: dbus_connection_send_with_reply failed\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_dbus_call_screencast_method: dbus_connection_send_with_reply failed"); dbus_message_unref(msg); return false; } @@ -339,7 +340,7 @@ static bool gsr_dbus_call_screencast_method(gsr_dbus *self, const char *method_n msg = dbus_pending_call_steal_reply(pending); if(!msg) { - fprintf(stderr, "gsr error: gsr_dbus_call_screencast_method: dbus_pending_call_steal_reply failed\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_dbus_call_screencast_method: dbus_pending_call_steal_reply failed"); dbus_pending_call_unref(pending); dbus_message_unref(msg); return false; @@ -350,7 +351,7 @@ static bool gsr_dbus_call_screencast_method(gsr_dbus *self, const char *method_n DBusMessageIter resp_args; if(!dbus_message_iter_init(msg, &resp_args)) { - fprintf(stderr, "gsr error: gsr_dbus_call_screencast_method: response message is missing arguments\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_dbus_call_screencast_method: response message is missing arguments"); dbus_message_unref(msg); return false; } else if (DBUS_TYPE_OBJECT_PATH == dbus_message_iter_get_arg_type(&resp_args)) { @@ -365,13 +366,13 @@ static bool gsr_dbus_call_screencast_method(gsr_dbus *self, const char *method_n } else if(DBUS_TYPE_STRING == dbus_message_iter_get_arg_type(&resp_args)) { char *err = NULL; dbus_message_iter_get_basic(&resp_args, &err); - fprintf(stderr, "gsr error: gsr_dbus_call_screencast_method: failed with error: %s\n", err); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_dbus_call_screencast_method: failed with error: %s", err); dbus_message_unref(msg); return false; // TODO: Check dbus_error_is_set? } else { - fprintf(stderr, "gsr error: gsr_dbus_call_screencast_method: response message is not an object path or unix fd\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_dbus_call_screencast_method: response message is not an object path or unix fd"); dbus_message_unref(msg); return false; // TODO: Check dbus_error_is_set? @@ -404,7 +405,7 @@ static bool gsr_dbus_call_screencast_method(gsr_dbus *self, const char *method_n static int gsr_dbus_get_response_status(DBusMessageIter *resp_args) { if(dbus_message_iter_get_arg_type(resp_args) != DBUS_TYPE_UINT32) { - fprintf(stderr, "gsr error: gsr_dbus_get_response_status: missing uint32 in response\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_dbus_get_response_status: missing uint32 in response"); return -1; } @@ -425,7 +426,7 @@ static dict_entry* find_dict_entry_by_key(dict_entry *entries, int num_entries, static bool gsr_dbus_get_variant_value(DBusMessageIter *iter, dict_entry *entry) { if(dbus_message_iter_get_arg_type(iter) != DBUS_TYPE_VARIANT) { - fprintf(stderr, "gsr error: gsr_dbus_get_variant_value: value is not a variant\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_dbus_get_variant_value: value is not a variant"); return false; } @@ -435,7 +436,7 @@ static bool gsr_dbus_get_variant_value(DBusMessageIter *iter, dict_entry *entry) switch(dbus_message_iter_get_arg_type(&variant_iter)) { case DBUS_TYPE_STRING: { if(entry->value_type != DICT_TYPE_STRING) { - fprintf(stderr, "gsr error: gsr_dbus_get_variant_value: expected entry value to be a(n) %s was a string\n", dict_value_type_to_string(entry->value_type)); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_dbus_get_variant_value: expected entry value to be a(n) %s was a string", dict_value_type_to_string(entry->value_type)); return false; } @@ -443,7 +444,7 @@ static bool gsr_dbus_get_variant_value(DBusMessageIter *iter, dict_entry *entry) dbus_message_iter_get_basic(&variant_iter, &value); if(!value) { - fprintf(stderr, "gsr error: gsr_dbus_get_variant_value: failed to get entry value as value\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_dbus_get_variant_value: failed to get entry value as value"); return false; } @@ -454,14 +455,14 @@ static bool gsr_dbus_get_variant_value(DBusMessageIter *iter, dict_entry *entry) entry->str = strdup(value); if(!entry->str) { - fprintf(stderr, "gsr error: gsr_dbus_get_variant_value: failed to copy value\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_dbus_get_variant_value: failed to copy value"); return false; } return true; } case DBUS_TYPE_UINT32: { if(entry->value_type != DICT_TYPE_UINT32) { - fprintf(stderr, "gsr error: gsr_dbus_get_variant_value: expected entry value to be a(n) %s was an uint32\n", dict_value_type_to_string(entry->value_type)); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_dbus_get_variant_value: expected entry value to be a(n) %s was an uint32", dict_value_type_to_string(entry->value_type)); return false; } @@ -470,7 +471,7 @@ static bool gsr_dbus_get_variant_value(DBusMessageIter *iter, dict_entry *entry) } case DBUS_TYPE_BOOLEAN: { if(entry->value_type != DICT_TYPE_BOOL) { - fprintf(stderr, "gsr error: gsr_dbus_get_variant_value: expected entry value to be a(n) %s was a boolean\n", dict_value_type_to_string(entry->value_type)); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_dbus_get_variant_value: expected entry value to be a(n) %s was a boolean", dict_value_type_to_string(entry->value_type)); return false; } @@ -479,7 +480,7 @@ static bool gsr_dbus_get_variant_value(DBusMessageIter *iter, dict_entry *entry) } } - fprintf(stderr, "gsr error: gsr_dbus_get_variant_value: got unexpected type, expected string, uint32 or boolean\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_dbus_get_variant_value: got unexpected type, expected string, uint32 or boolean"); return false; } @@ -492,7 +493,7 @@ static bool gsr_dbus_get_variant_value(DBusMessageIter *iter, dict_entry *entry) */ static bool gsr_dbus_get_map(DBusMessageIter *resp_args, dict_entry *entries, int num_entries) { if(dbus_message_iter_get_arg_type(resp_args) != DBUS_TYPE_ARRAY) { - fprintf(stderr, "gsr error: gsr_dbus_get_map: missing array in response\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_dbus_get_map: missing array in response"); return false; } @@ -508,20 +509,20 @@ static bool gsr_dbus_get_map(DBusMessageIter *resp_args, dict_entry *entries, in // dbus_message_iter_get_arg_type(&subiter), // dbus_message_iter_get_signature(&subiter)); if(dbus_message_iter_get_arg_type(&subiter) != DBUS_TYPE_DICT_ENTRY) { - fprintf(stderr, "gsr error: gsr_dbus_get_map: array value is not an entry\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_dbus_get_map: array value is not an entry"); return false; } dbus_message_iter_recurse(&subiter, &dictiter); if(dbus_message_iter_get_arg_type(&dictiter) != DBUS_TYPE_STRING) { - fprintf(stderr, "gsr error: gsr_dbus_get_map: entry key is not a string\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_dbus_get_map: entry key is not a string"); goto error; } dbus_message_iter_get_basic(&dictiter, &key); if(!key) { - fprintf(stderr, "gsr error: gsr_dbus_get_map: failed to get entry key as value\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_dbus_get_map: failed to get entry key as value"); goto error; } @@ -532,7 +533,7 @@ static bool gsr_dbus_get_map(DBusMessageIter *resp_args, dict_entry *entries, in } if(!dbus_message_iter_next(&dictiter)) { - fprintf(stderr, "gsr error: gsr_dbus_get_map: missing entry value\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_dbus_get_map: missing entry value"); goto error; } @@ -575,7 +576,7 @@ int gsr_dbus_screencast_create_session(gsr_dbus *self, char **session_handle) { DBusMessage *response_msg = NULL; if(!gsr_dbus_call_screencast_method(self, "CreateSession", NULL, NULL, args, 2, NULL, &response_msg)) { - fprintf(stderr, "gsr error: gsr_dbus_screencast_create_session: failed to setup ScreenCast session. Make sure you have a desktop portal running with support for the ScreenCast interface and that the desktop portal matches the Wayland compositor you are running.\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_dbus_screencast_create_session: failed to setup ScreenCast session. Make sure you have a desktop portal running with support for the ScreenCast interface and that the desktop portal matches the Wayland compositor you are running."); return -1; } @@ -584,7 +585,7 @@ int gsr_dbus_screencast_create_session(gsr_dbus *self, char **session_handle) { //fprintf(stderr, "signature: %s, sender: %s\n", dbus_message_get_signature(msg), dbus_message_get_sender(msg)); DBusMessageIter resp_args; if(!dbus_message_iter_init(response_msg, &resp_args)) { - fprintf(stderr, "gsr error: gsr_dbus_screencast_create_session: missing response\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_dbus_screencast_create_session: missing response"); dbus_message_unref(response_msg); return -1; } @@ -605,7 +606,7 @@ int gsr_dbus_screencast_create_session(gsr_dbus *self, char **session_handle) { } if(!entries[0].str) { - fprintf(stderr, "gsr error: gsr_dbus_screencast_create_session: missing \"session_handle\" in response\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_dbus_screencast_create_session: missing \"session_handle\" in response"); dbus_message_unref(response_msg); return -1; } @@ -644,16 +645,16 @@ int gsr_dbus_screencast_select_sources(gsr_dbus *self, const char *session_handl uint32_t available_source_types = 0; gsr_dbus_desktop_portal_get_property(self, "org.freedesktop.portal.ScreenCast", "AvailableSourceTypes", &available_source_types); if(available_source_types == 0) - fprintf(stderr, "gsr error: gsr_dbus_screencast_select_sources: no source types are available\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_dbus_screencast_select_sources: no source types are available"); capture_type = unset_unsupported_capture_types(capture_type, available_source_types); uint32_t available_cursor_modes = 0; gsr_dbus_desktop_portal_get_property(self, "org.freedesktop.portal.ScreenCast", "AvailableCursorModes", &available_cursor_modes); if(available_cursor_modes == 0) - fprintf(stderr, "gsr warning: gsr_dbus_screencast_select_sources: no cursors modes are available\n"); + gsr_log(GSR_LOG_LEVEL_WARNING, "gsr_dbus_screencast_select_sources: no cursors modes are available"); if(cursor_mode == GSR_PORTAL_CURSOR_MODE_METADATA && !(available_cursor_modes & GSR_PORTAL_CURSOR_MODE_METADATA)) { - fprintf(stderr, "gsr warning: gsr_dbus_screencast_select_sources: cursor mode metadata is not available, using cursor mode embedded instead\n"); + gsr_log(GSR_LOG_LEVEL_WARNING, "gsr_dbus_screencast_select_sources: cursor mode metadata is not available, using cursor mode embedded instead"); cursor_mode = GSR_PORTAL_CURSOR_MODE_EMBEDDED; } cursor_mode = unset_unsupported_cursor_modes(cursor_mode, available_cursor_modes); @@ -696,14 +697,14 @@ int gsr_dbus_screencast_select_sources(gsr_dbus *self, const char *session_handl args[5].str = self->screencast_restore_token; } } else if(self->screencast_restore_token && self->screencast_restore_token[0]) { - fprintf(stderr, "gsr warning: gsr_dbus_screencast_select_sources: tried to use restore token but this option is only available in screencast version >= 4, your wayland compositors screencast version is %d\n", screencast_server_version); + gsr_log(GSR_LOG_LEVEL_WARNING, "gsr_dbus_screencast_select_sources: tried to use restore token but this option is only available in screencast version >= 4, your wayland compositors screencast version is %d", screencast_server_version); } DBusMessage *response_msg = NULL; if(!gsr_dbus_call_screencast_method(self, "SelectSources", session_handle, NULL, args, num_arg_dict, NULL, &response_msg)) { if(num_arg_dict == 6) { /* We dont know what the error exactly is but assume it may be because of invalid restore token. In that case try without restore token */ - fprintf(stderr, "gsr warning: gsr_dbus_screencast_select_sources: SelectSources failed, retrying without restore_token\n"); + gsr_log(GSR_LOG_LEVEL_WARNING, "gsr_dbus_screencast_select_sources: SelectSources failed, retrying without restore_token"); num_arg_dict = 5; if(!gsr_dbus_call_screencast_method(self, "SelectSources", session_handle, NULL, args, num_arg_dict, NULL, &response_msg)) return -1; @@ -716,7 +717,7 @@ int gsr_dbus_screencast_select_sources(gsr_dbus *self, const char *session_handl //fprintf(stderr, "signature: %s, sender: %s\n", dbus_message_get_signature(msg), dbus_message_get_sender(msg)); DBusMessageIter resp_args; if(!dbus_message_iter_init(response_msg, &resp_args)) { - fprintf(stderr, "gsr error: gsr_dbus_screencast_create_session: missing response\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_dbus_screencast_create_session: missing response"); dbus_message_unref(response_msg); return -1; } @@ -770,7 +771,7 @@ int gsr_dbus_screencast_start(gsr_dbus *self, const char *session_handle, uint32 //fprintf(stderr, "signature: %s, sender: %s\n", dbus_message_get_signature(msg), dbus_message_get_sender(msg)); DBusMessageIter resp_args; if(!dbus_message_iter_init(response_msg, &resp_args)) { - fprintf(stderr, "gsr error: gsr_dbus_screencast_start: missing response\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_dbus_screencast_start: missing response"); dbus_message_unref(response_msg); return -1; } @@ -782,7 +783,7 @@ int gsr_dbus_screencast_start(gsr_dbus *self, const char *session_handle, uint32 } if(dbus_message_iter_get_arg_type(&resp_args) != DBUS_TYPE_ARRAY) { - fprintf(stderr, "gsr error: gsr_dbus_screencast_start: missing array in response\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_dbus_screencast_start: missing array in response"); dbus_message_unref(response_msg); return -1; } @@ -798,31 +799,31 @@ int gsr_dbus_screencast_start(gsr_dbus *self, const char *session_handle, uint32 // dbus_message_iter_get_arg_type(&subiter), // dbus_message_iter_get_signature(&subiter)); if(dbus_message_iter_get_arg_type(&subiter) != DBUS_TYPE_DICT_ENTRY) { - fprintf(stderr, "gsr error: gsr_dbus_screencast_start: array value is not an entry\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_dbus_screencast_start: array value is not an entry"); goto error; } dbus_message_iter_recurse(&subiter, &dictiter); if(dbus_message_iter_get_arg_type(&dictiter) != DBUS_TYPE_STRING) { - fprintf(stderr, "gsr error: gsr_dbus_screencast_start: entry key is not a string\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_dbus_screencast_start: entry key is not a string"); goto error; } dbus_message_iter_get_basic(&dictiter, &key); if(!key) { - fprintf(stderr, "gsr error: gsr_dbus_screencast_start: failed to get entry key as value\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_dbus_screencast_start: failed to get entry key as value"); goto error; } if(strcmp(key, "restore_token") == 0) { if(!dbus_message_iter_next(&dictiter)) { - fprintf(stderr, "gsr error: gsr_dbus_screencast_start: missing restore_token value\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_dbus_screencast_start: missing restore_token value"); goto error; } if(dbus_message_iter_get_arg_type(&dictiter) != DBUS_TYPE_VARIANT) { - fprintf(stderr, "gsr error: gsr_dbus_screencast_start: restore_token is not a variant\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_dbus_screencast_start: restore_token is not a variant"); goto error; } @@ -830,7 +831,7 @@ int gsr_dbus_screencast_start(gsr_dbus *self, const char *session_handle, uint32 dbus_message_iter_recurse(&dictiter, &variant_iter); if(dbus_message_iter_get_arg_type(&variant_iter) != DBUS_TYPE_STRING) { - fprintf(stderr, "gsr error: gsr_dbus_screencast_start: restore_token is not a string\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_dbus_screencast_start: restore_token is not a string"); goto error; } @@ -847,12 +848,12 @@ int gsr_dbus_screencast_start(gsr_dbus *self, const char *session_handle, uint32 } } else if(strcmp(key, "streams") == 0) { if(!dbus_message_iter_next(&dictiter)) { - fprintf(stderr, "gsr error: gsr_dbus_screencast_start: missing streams value\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_dbus_screencast_start: missing streams value"); goto error; } if(dbus_message_iter_get_arg_type(&dictiter) != DBUS_TYPE_VARIANT) { - fprintf(stderr, "gsr error: gsr_dbus_screencast_start: streams value is not a variant\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_dbus_screencast_start: streams value is not a variant"); goto error; } @@ -860,7 +861,7 @@ int gsr_dbus_screencast_start(gsr_dbus *self, const char *session_handle, uint32 dbus_message_iter_recurse(&dictiter, &variant_iter); if(dbus_message_iter_get_arg_type(&variant_iter) != DBUS_TYPE_ARRAY) { - fprintf(stderr, "gsr error: gsr_dbus_screencast_start: streams value is not an array\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_dbus_screencast_start: streams value is not an array"); goto error; } @@ -881,7 +882,7 @@ int gsr_dbus_screencast_start(gsr_dbus *self, const char *session_handle, uint32 } if(*pipewire_node == 0) { - fprintf(stderr, "gsr error: gsr_dbus_screencast_start: no pipewire node returned\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_dbus_screencast_start: no pipewire node returned"); goto error; } @@ -1,4 +1,5 @@ #include "../include/egl.h" +#include "../include/log.h" #include "../include/window/window.h" #include "../include/library_loader.h" #include "../include/utils.h" @@ -8,7 +9,6 @@ #include <stdlib.h> #include <dlfcn.h> #include <assert.h> -#include <unistd.h> // TODO: rename gsr_egl to something else since this includes both egl and glx and in the future maybe vulkan too @@ -53,34 +53,34 @@ static bool gsr_egl_create_window(gsr_egl *self, bool enable_debug) { self->egl_display = self->eglGetDisplay((EGLNativeDisplayType)gsr_window_get_display(self->window)); if(!self->egl_display) { - fprintf(stderr, "gsr error: gsr_egl_create_window failed: eglGetDisplay failed\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_egl_create_window failed: eglGetDisplay failed"); goto fail; } if(!self->eglInitialize(self->egl_display, NULL, NULL)) { - fprintf(stderr, "gsr error: gsr_egl_create_window failed: eglInitialize failed\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_egl_create_window failed: eglInitialize failed"); goto fail; } if(!self->eglChooseConfig(self->egl_display, attr, &ecfg, 1, &num_config) || num_config != 1) { - fprintf(stderr, "gsr error: gsr_egl_create_window failed: failed to find a matching config\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_egl_create_window failed: failed to find a matching config"); goto fail; } self->egl_context = self->eglCreateContext(self->egl_display, ecfg, NULL, ctxattr); if(!self->egl_context) { - fprintf(stderr, "gsr error: gsr_egl_create_window failed: failed to create egl context\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_egl_create_window failed: failed to create egl context"); goto fail; } self->egl_surface = self->eglCreateWindowSurface(self->egl_display, ecfg, (EGLNativeWindowType)gsr_window_get_window(self->window), NULL); if(!self->egl_surface) { - fprintf(stderr, "gsr error: gsr_egl_create_window failed: failed to create window surface\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_egl_create_window failed: failed to create window surface"); goto fail; } if(!self->eglMakeCurrent(self->egl_display, self->egl_surface, self->egl_surface, self->egl_context)) { - fprintf(stderr, "gsr error: gsr_egl_create_window failed: failed to make egl context current\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_egl_create_window failed: failed to make egl context current"); goto fail; } @@ -141,7 +141,7 @@ static bool gsr_egl_switch_to_glx_context(gsr_egl *self) { self->glx_fb_config = glx_fb_config_choose(self, display); if(!self->glx_fb_config) { - fprintf(stderr, "gsr error: gsr_egl_create_window failed: failed to find a suitable fb config\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_egl_create_window failed: failed to find a suitable fb config"); goto fail; } @@ -149,12 +149,12 @@ static bool gsr_egl_switch_to_glx_context(gsr_egl *self) { //self->glx_context = self->glXCreateContextAttribsARB(display, self->glx_fb_config, NULL, True, context_attrib_list); self->glx_context = self->glXCreateNewContext(display, self->glx_fb_config, GLX_RGBA_TYPE, NULL, True); if(!self->glx_context) { - fprintf(stderr, "gsr error: gsr_egl_create_window failed: failed to create glx context\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_egl_create_window failed: failed to create glx context"); goto fail; } if(!self->glXMakeContextCurrent(display, window, window, self->glx_context)) { - fprintf(stderr, "gsr error: gsr_egl_create_window failed: failed to make glx context current\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_egl_create_window failed: failed to make glx context current"); goto fail; } @@ -193,7 +193,7 @@ static bool gsr_egl_load_egl(gsr_egl *self, void *library) { }; if(!dlsym_load_list(library, required_dlsym)) { - fprintf(stderr, "gsr error: gsr_egl_load failed: missing required symbols in libEGL.so.1\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_egl_load failed: missing required symbols in libEGL.so.1"); return false; } @@ -221,17 +221,17 @@ static bool gsr_egl_proc_load_egl(gsr_egl *self) { self->glSignalSemaphoreEXT = (FUNC_glSignalSemaphoreEXT)self->eglGetProcAddress("glSignalSemaphoreEXT"); if(!self->eglExportDMABUFImageQueryMESA) { - fprintf(stderr, "gsr error: gsr_egl_load failed: could not find eglExportDMABUFImageQueryMESA\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_egl_load failed: could not find eglExportDMABUFImageQueryMESA"); return false; } if(!self->eglExportDMABUFImageMESA) { - fprintf(stderr, "gsr error: gsr_egl_load failed: could not find eglExportDMABUFImageMESA\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_egl_load failed: could not find eglExportDMABUFImageMESA"); return false; } if(!self->glEGLImageTargetTexture2DOES) { - fprintf(stderr, "gsr error: gsr_egl_load failed: could not find glEGLImageTargetTexture2DOES\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_egl_load failed: could not find glEGLImageTargetTexture2DOES"); return false; } @@ -251,13 +251,13 @@ static bool gsr_egl_load_glx(gsr_egl *self, void *library) { }; if(!dlsym_load_list(library, required_dlsym)) { - fprintf(stderr, "gsr error: gsr_egl_load failed: missing required symbols in libGLX.so.0\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_egl_load failed: missing required symbols in libGLX.so.0"); return false; } self->glXCreateContextAttribsARB = (FUNC_glXCreateContextAttribsARB)self->glXGetProcAddress((const unsigned char*)"glXCreateContextAttribsARB"); if(!self->glXCreateContextAttribsARB) { - fprintf(stderr, "gsr error: gsr_egl_load_glx failed: could not find glXCreateContextAttribsARB\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_egl_load_glx failed: could not find glXCreateContextAttribsARB"); return false; } @@ -343,7 +343,7 @@ static bool gsr_egl_load_gl(gsr_egl *self, void *library) { }; if(!dlsym_load_list(library, required_dlsym)) { - fprintf(stderr, "gsr error: gsr_egl_load failed: missing required symbols in libGL.so.1\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_egl_load failed: missing required symbols in libGL.so.1"); return false; } @@ -358,7 +358,7 @@ static void debug_callback(unsigned int source, unsigned int type, unsigned int (void)length; (void)userParam; if(severity != GL_DEBUG_SEVERITY_NOTIFICATION) - fprintf(stderr, "gsr info: gl callback: %s type = 0x%x, severity = 0x%x, message = %s\n", type == GL_DEBUG_TYPE_ERROR ? "** GL ERROR **" : "", type, severity, message); + gsr_log(GSR_LOG_LEVEL_INFO, "gl callback: %s type = 0x%x, severity = 0x%x, message = %s", type == GL_DEBUG_TYPE_ERROR ? "** GL ERROR **" : "", type, severity, message); } /* TODO: check for glx swap control extension string (GLX_EXT_swap_control, etc) */ @@ -378,12 +378,12 @@ static void set_vertical_sync_enabled(gsr_egl *egl, int enabled) { static int warned = 0; if (!warned) { warned = 1; - fprintf(stderr, "gsr warning: setting vertical sync not supported\n"); + gsr_log(GSR_LOG_LEVEL_WARNING, "setting vertical sync not supported"); } } if(result != 0) - fprintf(stderr, "gsr warning: setting vertical sync failed\n"); + gsr_log(GSR_LOG_LEVEL_WARNING, "setting vertical sync failed"); } static void gsr_egl_disable_vsync(gsr_egl *self) { @@ -407,7 +407,7 @@ bool gsr_egl_load(gsr_egl *self, gsr_window *window, bool is_monitor_capture, bo dlerror(); /* clear */ self->egl_library = dlopen("libEGL.so.1", RTLD_LAZY); if(!self->egl_library) { - fprintf(stderr, "gsr error: gsr_egl_load: failed to load libEGL.so.1, error: %s\n", dlerror()); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_egl_load: failed to load libEGL.so.1, error: %s", dlerror()); goto fail; } @@ -415,7 +415,7 @@ bool gsr_egl_load(gsr_egl *self, gsr_window *window, bool is_monitor_capture, bo self->gl_library = dlopen("libGL.so.1", RTLD_LAZY); if(!self->gl_library) { - fprintf(stderr, "gsr error: gsr_egl_load: failed to load libGL.so.1, error: %s\n", dlerror()); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_egl_load: failed to load libGL.so.1, error: %s", dlerror()); goto fail; } diff --git a/src/encoder/encoder.c b/src/encoder/encoder.c index 9ce7d19..e9b71d7 100644 --- a/src/encoder/encoder.c +++ b/src/encoder/encoder.c @@ -1,5 +1,7 @@ #include "../../include/encoder/encoder.h" +#include "../../include/log.h" #include "../../include/utils.h" +#include "../../include/ffmpeg_utils.h" #include <string.h> #include <stdio.h> @@ -25,7 +27,7 @@ static void gsr_write_first_frame_timestamp_file(const char *filepath) { FILE *file = fopen(filepath, "w"); if(!file) { - fprintf(stderr, "gsr warning: failed to open timestamp file '%s': %s\n", filepath, strerror(errno)); + gsr_log(GSR_LOG_LEVEL_WARNING, "failed to open timestamp file '%s': %s", filepath, strerror(errno)); return; } @@ -41,15 +43,15 @@ bool gsr_encoder_init(gsr_encoder *self, gsr_replay_storage replay_storage, size self->first_pts = -1; if(pthread_mutex_init(&self->file_write_mutex, NULL) != 0) { - fprintf(stderr, "gsr error: gsr_encoder_init: failed to create mutex\n"); - gsr_encoder_deinit(self); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_encoder_init: failed to create mutex"); + gsr_encoder_deinit(self, false); return false; } self->file_write_mutex_created = true; if(pthread_mutex_init(&self->replay_mutex, NULL) != 0) { - fprintf(stderr, "gsr error: gsr_encoder_init: failed to create mutex\n"); - gsr_encoder_deinit(self); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_encoder_init: failed to create mutex"); + gsr_encoder_deinit(self, false); return false; } self->replay_mutex_created = true; @@ -57,8 +59,8 @@ bool gsr_encoder_init(gsr_encoder *self, gsr_replay_storage replay_storage, size if(replay_buffer_num_packets > 0) { self->replay_buffer = gsr_replay_buffer_create(replay_storage, replay_directory, replay_buffer_time, replay_buffer_num_packets); if(!self->replay_buffer) { - fprintf(stderr, "gsr error: gsr_encoder_init: failed to create replay buffer\n"); - gsr_encoder_deinit(self); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_encoder_init: failed to create replay buffer"); + gsr_encoder_deinit(self, false); return false; } } @@ -66,7 +68,7 @@ bool gsr_encoder_init(gsr_encoder *self, gsr_replay_storage replay_storage, size return true; } -void gsr_encoder_deinit(gsr_encoder *self) { +void gsr_encoder_deinit(gsr_encoder *self, bool exiting) { if(self->file_write_mutex_created) pthread_mutex_lock(&self->file_write_mutex); for(size_t i = 0; i < self->num_recording_destinations; ++i) { @@ -79,7 +81,10 @@ void gsr_encoder_deinit(gsr_encoder *self) { if(self->replay_buffer) { pthread_mutex_lock(&self->replay_mutex); - gsr_replay_buffer_destroy(self->replay_buffer); + if(exiting) + gsr_replay_buffer_destroy_at_exit(self->replay_buffer); + else + gsr_replay_buffer_destroy(self->replay_buffer); self->replay_buffer = NULL; pthread_mutex_unlock(&self->replay_mutex); } @@ -120,7 +125,7 @@ void gsr_encoder_receive_packets(gsr_encoder *self, AVCodecContext *codec_contex pthread_mutex_lock(&self->replay_mutex); const double time_now = clock_get_monotonic_seconds(); if(!gsr_replay_buffer_append(self->replay_buffer, av_packet, time_now)) - fprintf(stderr, "gsr error: gsr_encoder_receive_packets: failed to add replay buffer data\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_encoder_receive_packets: failed to add replay buffer data"); pthread_mutex_unlock(&self->replay_mutex); } @@ -151,11 +156,13 @@ void gsr_encoder_receive_packets(gsr_encoder *self, AVCodecContext *codec_contex // TODO: Is av_interleaved_write_frame needed?. Answer: might be needed for mkv but dont use it! it causes frames to be inconsistent, skipping frames and duplicating frames. // TODO: av_interleaved_write_frame might be needed for cfr, or always for flv const int ret = av_write_frame(recording_destination->format_context, av_packet); + if(ret >= 0) + gsr_av_format_context_mark_packet_written(recording_destination->format_context); if(ret < 0) { char error_buffer[AV_ERROR_MAX_STRING_SIZE]; if(av_strerror(ret, error_buffer, sizeof(error_buffer)) < 0) snprintf(error_buffer, sizeof(error_buffer), "Unknown error"); - fprintf(stderr, "gsr error: gsr_encoder_receive_packets: failed to write frame index %d to muxer, reason: %s (%d)\n", av_packet->stream_index, error_buffer, ret); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_encoder_receive_packets: failed to write frame index %d to muxer, reason: %s (%d)", av_packet->stream_index, error_buffer, ret); } } pthread_mutex_unlock(&self->file_write_mutex); @@ -167,11 +174,11 @@ void gsr_encoder_receive_packets(gsr_encoder *self, AVCodecContext *codec_contex break; } else if (res == AVERROR_EOF) { // this is the end of the stream av_packet_free(&av_packet); - fprintf(stderr, "End of stream!\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_encoder_receive_packets: end of stream"); break; } else { av_packet_free(&av_packet); - fprintf(stderr, "Unexpected error: %d\n", res); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_encoder_receive_packets: unexpected error: %d", res); break; } } @@ -179,13 +186,13 @@ void gsr_encoder_receive_packets(gsr_encoder *self, AVCodecContext *codec_contex size_t gsr_encoder_add_recording_destination(gsr_encoder *self, AVCodecContext *codec_context, AVFormatContext *format_context, AVStream *stream, int64_t start_pts) { if(self->num_recording_destinations >= GSR_MAX_RECORDING_DESTINATIONS) { - fprintf(stderr, "gsr error: gsr_encoder_add_recording_destination: failed to add destination, reached the max amount of recording destinations (%d)\n", GSR_MAX_RECORDING_DESTINATIONS); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_encoder_add_recording_destination: failed to add destination, reached the max amount of recording destinations (%d)", GSR_MAX_RECORDING_DESTINATIONS); return (size_t)-1; } for(size_t i = 0; i < self->num_recording_destinations; ++i) { if(self->recording_destinations[i].stream == stream) { - fprintf(stderr, "gsr error: gsr_encoder_add_recording_destination: failed to add destination, the stream %p already exists as an output\n", (void*)stream); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_encoder_add_recording_destination: failed to add destination, the stream %p already exists as an output", (void*)stream); return (size_t)-1; } } diff --git a/src/encoder/video/nvenc.c b/src/encoder/video/nvenc.c index dd877c5..594416c 100644 --- a/src/encoder/video/nvenc.c +++ b/src/encoder/video/nvenc.c @@ -1,4 +1,5 @@ #include "../../../include/encoder/video/nvenc.h" +#include "../../../include/log.h" #include "../../../include/egl.h" #include "../../../include/cuda.h" #include "../../../include/utils.h" @@ -25,7 +26,7 @@ typedef struct { static bool gsr_video_encoder_nvenc_setup_context(gsr_video_encoder_nvenc *self, AVCodecContext *video_codec_context) { self->device_ctx = av_hwdevice_ctx_alloc(AV_HWDEVICE_TYPE_CUDA); if(!self->device_ctx) { - fprintf(stderr, "gsr error: gsr_video_encoder_nvenc_setup_context failed: failed to create hardware device context\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_video_encoder_nvenc_setup_context failed: failed to create hardware device context"); return false; } @@ -33,14 +34,14 @@ static bool gsr_video_encoder_nvenc_setup_context(gsr_video_encoder_nvenc *self, AVCUDADeviceContext *cuda_device_context = (AVCUDADeviceContext*)hw_device_context->hwctx; cuda_device_context->cuda_ctx = self->cuda.cu_ctx; if(av_hwdevice_ctx_init(self->device_ctx) < 0) { - fprintf(stderr, "gsr error: gsr_video_encoder_nvenc_setup_context failed: failed to create hardware device context\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_video_encoder_nvenc_setup_context failed: failed to create hardware device context"); av_buffer_unref(&self->device_ctx); return false; } AVBufferRef *frame_context = av_hwframe_ctx_alloc(self->device_ctx); if(!frame_context) { - fprintf(stderr, "gsr error: gsr_video_encoder_nvenc_setup_context failed: failed to create hwframe context\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_video_encoder_nvenc_setup_context failed: failed to create hwframe context"); av_buffer_unref(&self->device_ctx); return false; } @@ -53,8 +54,7 @@ static bool gsr_video_encoder_nvenc_setup_context(gsr_video_encoder_nvenc *self, hw_frame_context->device_ctx = (AVHWDeviceContext*)self->device_ctx->data; if (av_hwframe_ctx_init(frame_context) < 0) { - fprintf(stderr, "gsr error: gsr_video_encoder_nvenc_setup_context failed: failed to initialize hardware frame context " - "(note: ffmpeg version needs to be > 4.0)\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_video_encoder_nvenc_setup_context failed: failed to initialize hardware frame context (note: ffmpeg version needs to be > 4.0)"); av_buffer_unref(&self->device_ctx); //av_buffer_unref(&frame_context); return false; @@ -72,7 +72,7 @@ static bool cuda_register_opengl_texture(gsr_cuda *cuda, CUgraphicsResource *cud if (res != CUDA_SUCCESS) { const char *err_str = "unknown"; cuda->cuGetErrorString(res, &err_str); - fprintf(stderr, "gsr error: cuda_register_opengl_texture: cuGraphicsGLRegisterImage failed, error: %s, texture " "id: %u\n", err_str, texture_id); + gsr_log(GSR_LOG_LEVEL_ERROR, "cuda_register_opengl_texture: cuGraphicsGLRegisterImage failed, error: %s, texture id: %u", err_str, texture_id); return false; } @@ -86,7 +86,7 @@ static bool cuda_register_opengl_texture(gsr_cuda *cuda, CUgraphicsResource *cud static bool gsr_video_encoder_nvenc_setup_textures(gsr_video_encoder_nvenc *self, AVCodecContext *video_codec_context, AVFrame *frame) { const int res = av_hwframe_get_buffer(video_codec_context->hw_frames_ctx, frame, 0); if(res < 0) { - fprintf(stderr, "gsr error: gsr_video_encoder_nvenc_setup_textures: av_hwframe_get_buffer failed: %d\n", res); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_video_encoder_nvenc_setup_textures: av_hwframe_get_buffer failed: %d", res); return false; } @@ -99,7 +99,7 @@ static bool gsr_video_encoder_nvenc_setup_textures(gsr_video_encoder_nvenc *self self->target_texture_size[i] = (vec2i){ video_codec_context->width / div[i], video_codec_context->height / div[i] }; self->target_textures[i] = gl_create_texture(self->params.egl, self->target_texture_size[i].x, self->target_texture_size[i].y, self->params.color_depth == GSR_COLOR_DEPTH_8_BITS ? internal_formats_nv12[i] : internal_formats_p010[i], formats[i], GL_NEAREST); if(self->target_textures[i] == 0) { - fprintf(stderr, "gsr error: gsr_video_encoder_nvenc_setup_textures: failed to create opengl texture\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_video_encoder_nvenc_setup_textures: failed to create opengl texture"); return false; } @@ -117,7 +117,7 @@ static bool gsr_video_encoder_nvenc_start(gsr_video_encoder *encoder, AVCodecCon gsr_video_encoder_nvenc *self = encoder->priv; if(!gsr_cuda_load(&self->cuda)) { - fprintf(stderr, "gsr error: gsr_video_encoder_nvenc_start: failed to load cuda\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_video_encoder_nvenc_start: failed to load cuda"); gsr_video_encoder_nvenc_stop(self, video_codec_context); return false; } @@ -152,7 +152,7 @@ void gsr_video_encoder_nvenc_stop(gsr_video_encoder_nvenc *self, AVCodecContext self->target_textures[0] = 0; self->target_textures[1] = 0; - if(video_codec_context->hw_frames_ctx) + if(video_codec_context && video_codec_context->hw_frames_ctx) av_buffer_unref(&video_codec_context->hw_frames_ctx); if(self->device_ctx) av_buffer_unref(&self->device_ctx); diff --git a/src/encoder/video/software.c b/src/encoder/video/software.c index b473bb9..5ee3ab9 100644 --- a/src/encoder/video/software.c +++ b/src/encoder/video/software.c @@ -1,4 +1,5 @@ #include "../../../include/encoder/video/software.h" +#include "../../../include/log.h" #include "../../../include/egl.h" #include "../../../include/utils.h" @@ -19,13 +20,13 @@ typedef struct { static bool gsr_video_encoder_software_setup_textures(gsr_video_encoder_software *self, AVCodecContext *video_codec_context, AVFrame *frame) { int res = av_frame_get_buffer(frame, LINESIZE_ALIGNMENT); if(res < 0) { - fprintf(stderr, "gsr error: gsr_video_encoder_software_setup_textures: av_frame_get_buffer failed: %d\n", res); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_video_encoder_software_setup_textures: av_frame_get_buffer failed: %d", res); return false; } res = av_frame_make_writable(frame); if(res < 0) { - fprintf(stderr, "gsr error: gsr_video_encoder_software_setup_textures: av_frame_make_writable failed: %d\n", res); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_video_encoder_software_setup_textures: av_frame_make_writable failed: %d", res); return false; } @@ -38,7 +39,7 @@ static bool gsr_video_encoder_software_setup_textures(gsr_video_encoder_software self->texture_sizes[i] = (vec2i){ video_codec_context->width / div[i], video_codec_context->height / div[i] }; self->target_textures[i] = gl_create_texture(self->params.egl, self->texture_sizes[i].x, self->texture_sizes[i].y, self->params.color_depth == GSR_COLOR_DEPTH_8_BITS ? internal_formats_nv12[i] : internal_formats_p010[i], formats[i], GL_NEAREST); if(self->target_textures[i] == 0) { - fprintf(stderr, "gsr error: gsr_capture_kms_setup_cuda_textures: failed to create opengl texture\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_capture_kms_setup_cuda_textures: failed to create opengl texture"); return false; } } diff --git a/src/encoder/video/vaapi.c b/src/encoder/video/vaapi.c index 449fb1d..12dd3d7 100644 --- a/src/encoder/video/vaapi.c +++ b/src/encoder/video/vaapi.c @@ -1,4 +1,5 @@ #include "../../../include/encoder/video/vaapi.h" +#include "../../../include/log.h" #include "../../../include/utils.h" #include "../../../include/egl.h" @@ -28,18 +29,18 @@ typedef struct { static bool gsr_video_encoder_vaapi_setup_context(gsr_video_encoder_vaapi *self, AVCodecContext *video_codec_context) { char render_path[128]; if(!gsr_card_path_get_render_path(self->params.egl->card_path, render_path)) { - fprintf(stderr, "gsr error: gsr_video_encoder_vaapi_setup_context: failed to get /dev/dri/renderDXXX file from %s\n", self->params.egl->card_path); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_video_encoder_vaapi_setup_context: failed to get /dev/dri/renderDXXX file from %s", self->params.egl->card_path); return false; } if(av_hwdevice_ctx_create(&self->device_ctx, AV_HWDEVICE_TYPE_VAAPI, render_path, NULL, 0) < 0) { - fprintf(stderr, "gsr error: gsr_video_encoder_vaapi_setup_context: failed to create hardware device context\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_video_encoder_vaapi_setup_context: failed to create hardware device context"); return false; } AVBufferRef *frame_context = av_hwframe_ctx_alloc(self->device_ctx); if(!frame_context) { - fprintf(stderr, "gsr error: gsr_video_encoder_vaapi_setup_context: failed to create hwframe context\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_video_encoder_vaapi_setup_context: failed to create hwframe context"); av_buffer_unref(&self->device_ctx); return false; } @@ -57,8 +58,7 @@ static bool gsr_video_encoder_vaapi_setup_context(gsr_video_encoder_vaapi *self, self->va_dpy = vactx->display; if (av_hwframe_ctx_init(frame_context) < 0) { - fprintf(stderr, "gsr error: gsr_video_encoder_vaapi_setup_context: failed to initialize hardware frame context " - "(note: ffmpeg version needs to be > 4.0)\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_video_encoder_vaapi_setup_context: failed to initialize hardware frame context (note: ffmpeg version needs to be > 4.0)"); av_buffer_unref(&self->device_ctx); //av_buffer_unref(&frame_context); return false; @@ -76,7 +76,7 @@ static uint32_t fourcc(uint32_t a, uint32_t b, uint32_t c, uint32_t d) { static bool gsr_video_encoder_vaapi_setup_textures(gsr_video_encoder_vaapi *self, AVCodecContext *video_codec_context, AVFrame *frame) { const int res = av_hwframe_get_buffer(video_codec_context->hw_frames_ctx, frame, 0); if(res < 0) { - fprintf(stderr, "gsr error: gsr_video_encoder_vaapi_setup_textures: av_hwframe_get_buffer failed: %d\n", res); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_video_encoder_vaapi_setup_textures: av_hwframe_get_buffer failed: %d", res); return false; } @@ -84,7 +84,7 @@ static bool gsr_video_encoder_vaapi_setup_textures(gsr_video_encoder_vaapi *self VAStatus va_status = vaExportSurfaceHandle(self->va_dpy, target_surface_id, VA_SURFACE_ATTRIB_MEM_TYPE_DRM_PRIME_2, VA_EXPORT_SURFACE_WRITE_ONLY | VA_EXPORT_SURFACE_SEPARATE_LAYERS, &self->prime); if(va_status != VA_STATUS_SUCCESS) { - fprintf(stderr, "gsr error: gsr_video_encoder_vaapi_setup_textures: vaExportSurfaceHandle failed, error: %d\n", va_status); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_video_encoder_vaapi_setup_textures: vaExportSurfaceHandle failed, error: %d", va_status); return false; } vaSyncSurface(self->va_dpy, target_surface_id); @@ -120,7 +120,7 @@ static bool gsr_video_encoder_vaapi_setup_textures(gsr_video_encoder_vaapi *self while(self->params.egl->eglGetError() != EGL_SUCCESS){} EGLImage image = self->params.egl->eglCreateImage(self->params.egl->egl_display, 0, EGL_LINUX_DMA_BUF_EXT, NULL, img_attr); if(!image) { - fprintf(stderr, "gsr error: gsr_video_encoder_vaapi_setup_textures: failed to create egl image from drm fd for output drm fd, error: %d\n", self->params.egl->eglGetError()); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_video_encoder_vaapi_setup_textures: failed to create egl image from drm fd for output drm fd, error: %d", self->params.egl->eglGetError()); return false; } @@ -133,7 +133,7 @@ static bool gsr_video_encoder_vaapi_setup_textures(gsr_video_encoder_vaapi *self self->params.egl->glEGLImageTargetTexture2DOES(GL_TEXTURE_2D, image); if(self->params.egl->glGetError() != 0 || self->params.egl->eglGetError() != EGL_SUCCESS) { // TODO: Get the error properly - fprintf(stderr, "gsr error: gsr_video_encoder_vaapi_setup_textures: failed to bind egl image to gl texture, error: %d\n", self->params.egl->eglGetError()); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_video_encoder_vaapi_setup_textures: failed to bind egl image to gl texture, error: %d", self->params.egl->eglGetError()); self->params.egl->eglDestroyImage(self->params.egl->egl_display, image); self->params.egl->glBindTexture(GL_TEXTURE_2D, 0); return false; @@ -145,7 +145,7 @@ static bool gsr_video_encoder_vaapi_setup_textures(gsr_video_encoder_vaapi *self return true; } else { - fprintf(stderr, "gsr error: gsr_video_encoder_vaapi_setup_textures: unexpected fourcc %u for output drm fd, expected nv12 or p010\n", self->prime.fourcc); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_video_encoder_vaapi_setup_textures: unexpected fourcc %u for output drm fd, expected nv12 or p010", self->prime.fourcc); return false; } } @@ -165,26 +165,26 @@ static bool supports_hevc_without_padding(const char *card_path) { char render_path[128]; if(!gsr_card_path_get_render_path(card_path, render_path)) { - fprintf(stderr, "gsr error: supports_hevc_without_padding: failed to get /dev/dri/renderDXXX file from %s\n", card_path); + gsr_log(GSR_LOG_LEVEL_ERROR, "supports_hevc_without_padding: failed to get /dev/dri/renderDXXX file from %s", card_path); return false; } const int drm_fd = open(render_path, O_RDWR); if(drm_fd == -1) { - fprintf(stderr, "gsr error: supports_hevc_without_padding: failed to open device %s\n", render_path); + gsr_log(GSR_LOG_LEVEL_ERROR, "supports_hevc_without_padding: failed to open device %s", render_path); return false; } const VADisplay va_dpy = vaGetDisplayDRM(drm_fd); if(!va_dpy) { - fprintf(stderr, "gsr error: supports_hevc_without_padding: failed to get vaapi display for device %s\n", render_path); + gsr_log(GSR_LOG_LEVEL_ERROR, "supports_hevc_without_padding: failed to get vaapi display for device %s", render_path); goto done; } vaSetInfoCallback(va_dpy, NULL, NULL); if(vaInitialize(va_dpy, &va_major, &va_minor) != VA_STATUS_SUCCESS) { - fprintf(stderr, "gsr error: supports_hevc_without_padding: vaInitialize failed\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "supports_hevc_without_padding: vaInitialize failed"); goto done; } initialized = true; @@ -193,26 +193,26 @@ static bool supports_hevc_without_padding(const char *card_path) { if(va_status != VA_STATUS_SUCCESS) { va_status = vaCreateConfig(va_dpy, VAProfileHEVCMain, VAEntrypointEncSliceLP, NULL, 0, &va_config); if(va_status != VA_STATUS_SUCCESS) { - fprintf(stderr, "gsr error: supports_hevc_without_padding: failed to create hevc vaapi config, error: %s (%d)\n", vaErrorStr(va_status), va_status); + gsr_log(GSR_LOG_LEVEL_ERROR, "supports_hevc_without_padding: failed to create hevc vaapi config, error: %s (%d)", vaErrorStr(va_status), va_status); return false; } } va_status = vaQuerySurfaceAttributes(va_dpy, va_config, 0, &num_surface_attr); if(va_status != VA_STATUS_SUCCESS) { - fprintf(stderr, "gsr error: supports_hevc_without_padding: failed to query vaapi surface attributes size, error: %s (%d)\n", vaErrorStr(va_status), va_status); + gsr_log(GSR_LOG_LEVEL_ERROR, "supports_hevc_without_padding: failed to query vaapi surface attributes size, error: %s (%d)", vaErrorStr(va_status), va_status); goto done; } surface_attr_list = malloc(num_surface_attr * sizeof(VASurfaceAttrib)); if(!surface_attr_list) { - fprintf(stderr, "gsr error: supports_hevc_without_padding: failed to allocate memory for %u vaapi surface attributes, error: %s (%d)\n", num_surface_attr, vaErrorStr(va_status), va_status); + gsr_log(GSR_LOG_LEVEL_ERROR, "supports_hevc_without_padding: failed to allocate memory for %u vaapi surface attributes, error: %s (%d)", num_surface_attr, vaErrorStr(va_status), va_status); goto done; } va_status = vaQuerySurfaceAttributes(va_dpy, va_config, surface_attr_list, &num_surface_attr); if(va_status != VA_STATUS_SUCCESS) { - fprintf(stderr, "gsr error: supports_hevc_without_padding: failed to query vaapi surface attributes data, error: %s (%d)\n", vaErrorStr(va_status), va_status); + gsr_log(GSR_LOG_LEVEL_ERROR, "supports_hevc_without_padding: failed to query vaapi surface attributes data, error: %s (%d)", vaErrorStr(va_status), va_status); goto done; } @@ -264,7 +264,7 @@ static bool gsr_video_encoder_vaapi_start(gsr_video_encoder *encoder, AVCodecCon } if(FFALIGN(video_codec_context->width, 2) != FFALIGN(frame->width, 2) || FFALIGN(video_codec_context->height, 2) != FFALIGN(frame->height, 2)) { - fprintf(stderr, "gsr warning: gsr_video_encoder_vaapi_start: black bars have been added to the video because of a bug in AMD drivers/hardware. Record with h264/hevc codec instead (-k h264) to get around this issue\n"); + gsr_log(GSR_LOG_LEVEL_WARNING, "gsr_video_encoder_vaapi_start: black bars have been added to the video because of a bug in AMD drivers/hardware. Record with h264/hevc codec instead (-k h264) to get around this issue"); } if(video_codec_context->width < 128) @@ -294,7 +294,7 @@ void gsr_video_encoder_vaapi_stop(gsr_video_encoder_vaapi *self, AVCodecContext self->target_textures[0] = 0; self->target_textures[1] = 0; - if(video_codec_context->hw_frames_ctx) + if(video_codec_context && video_codec_context->hw_frames_ctx) av_buffer_unref(&video_codec_context->hw_frames_ctx); if(self->device_ctx) av_buffer_unref(&self->device_ctx); diff --git a/src/encoder/video/vulkan.c b/src/encoder/video/vulkan.c index 3b7c567..232a37d 100644 --- a/src/encoder/video/vulkan.c +++ b/src/encoder/video/vulkan.c @@ -1,4 +1,5 @@ #include "../../../include/encoder/video/vulkan.h" +#include "../../../include/log.h" #include "../../../include/utils.h" #include "../../../include/egl.h" @@ -36,6 +37,7 @@ typedef struct { PFN_vkResetFences vkResetFences; PFN_vkWaitForFences vkWaitForFences; PFN_vkGetDeviceQueue vkGetDeviceQueue; + PFN_vkGetDeviceQueue2 vkGetDeviceQueue2; PFN_vkQueueSubmit vkQueueSubmit; PFN_vkResetCommandBuffer vkResetCommandBuffer; PFN_vkCreateSemaphore vkCreateSemaphore; @@ -73,12 +75,12 @@ typedef struct { static bool gsr_vk_funcs_load(gsr_vk_funcs *vk, PFN_vkGetInstanceProcAddr get_inst_proc, VkInstance inst, VkDevice dev) { PFN_vkGetDeviceProcAddr get_dev_proc = (PFN_vkGetDeviceProcAddr)get_inst_proc(inst, "vkGetDeviceProcAddr"); if(!get_dev_proc) { - fprintf(stderr, "gsr error: gsr_vk_funcs_load: failed to load vkGetDeviceProcAddr\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_vk_funcs_load: failed to load vkGetDeviceProcAddr"); return false; } -#define LOAD_INST(name) vk->name = (PFN_##name)get_inst_proc(inst, #name); if(!vk->name) { fprintf(stderr, "gsr error: gsr_vk_funcs_load: failed to load " #name "\n"); return false; } -#define LOAD_DEV(name) vk->name = (PFN_##name)get_dev_proc(dev, #name); if(!vk->name) { fprintf(stderr, "gsr error: gsr_vk_funcs_load: failed to load " #name "\n"); return false; } +#define LOAD_INST(name) vk->name = (PFN_##name)get_inst_proc(inst, #name); if(!vk->name) { gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_vk_funcs_load: failed to load " #name); return false; } +#define LOAD_DEV(name) vk->name = (PFN_##name)get_dev_proc(dev, #name); if(!vk->name) { gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_vk_funcs_load: failed to load " #name); return false; } LOAD_INST(vkGetPhysicalDeviceMemoryProperties) LOAD_DEV(vkCreateImage) @@ -100,6 +102,7 @@ static bool gsr_vk_funcs_load(gsr_vk_funcs *vk, PFN_vkGetInstanceProcAddr get_in LOAD_DEV(vkResetFences) LOAD_DEV(vkWaitForFences) LOAD_DEV(vkGetDeviceQueue) + LOAD_DEV(vkGetDeviceQueue2) LOAD_DEV(vkQueueSubmit) LOAD_DEV(vkResetCommandBuffer) LOAD_DEV(vkCreateSemaphore) @@ -118,13 +121,13 @@ static bool gsr_video_encoder_vulkan_setup_context(gsr_video_encoder_vulkan *sel snprintf(device_index_str, sizeof(device_index_str), "%d", self->params.egl->vulkan_device_index); if(av_hwdevice_ctx_create(&self->device_ctx, AV_HWDEVICE_TYPE_VULKAN, device_index_str, options, 0) < 0) { - fprintf(stderr, "gsr error: gsr_video_encoder_vulkan_setup_context: failed to create hardware device context\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_video_encoder_vulkan_setup_context: failed to create hardware device context"); return false; } AVBufferRef *frame_context = av_hwframe_ctx_alloc(self->device_ctx); if(!frame_context) { - fprintf(stderr, "gsr error: gsr_video_encoder_vulkan_setup_context: failed to create hwframe context\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_video_encoder_vulkan_setup_context: failed to create hwframe context"); av_buffer_unref(&self->device_ctx); return false; } @@ -137,8 +140,7 @@ static bool gsr_video_encoder_vulkan_setup_context(gsr_video_encoder_vulkan *sel hw_frame_context->device_ctx = (AVHWDeviceContext*)self->device_ctx->data; if (av_hwframe_ctx_init(frame_context) < 0) { - fprintf(stderr, "gsr error: gsr_video_encoder_vulkan_setup_context: failed to initialize hardware frame context " - "(note: ffmpeg version needs to be > 4.0)\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_video_encoder_vulkan_setup_context: failed to initialize hardware frame context (note: ffmpeg version needs to be > 4.0)"); av_buffer_unref(&self->device_ctx); return false; } @@ -162,6 +164,16 @@ static AVVulkanDeviceContext* video_codec_context_get_vulkan_data(AVCodecContext return (AVVulkanDeviceContext*)device_context->hwctx; } +/* The flags that FFmpeg created the device queues with, which vkGetDeviceQueue2 has to be given to return the queue */ +static VkDeviceQueueCreateFlags get_device_queue_create_flags(AVVulkanDeviceContext *vv) { +#if LIBAVUTIL_VERSION_INT >= AV_VERSION_INT(61, 1, 100) + return vv->queue_flags; +#else + (void)vv; + return 0; +#endif +} + static int get_graphics_queue_family(AVVulkanDeviceContext *vv) { #if LIBAVUTIL_VERSION_INT >= AV_VERSION_INT(59, 39, 100) for(int i = 0; i < vv->nb_qf; i++) { @@ -230,7 +242,7 @@ static bool create_exportable_image( }; if(vk->vkCreateImage(dev, &img_info, NULL, out_image) != VK_SUCCESS) { - fprintf(stderr, "gsr error: create_exportable_image: vkCreateImage failed\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "create_exportable_image: vkCreateImage failed"); return false; } @@ -240,7 +252,7 @@ static bool create_exportable_image( uint32_t mem_type_idx = get_memory_type_idx(phys_dev, &mem_reqs, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, vk->vkGetPhysicalDeviceMemoryProperties); if(mem_type_idx == UINT32_MAX) { - fprintf(stderr, "gsr error: create_exportable_image: no suitable memory type\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "create_exportable_image: no suitable memory type"); vk->vkDestroyImage(dev, *out_image, NULL); *out_image = VK_NULL_HANDLE; return false; @@ -263,14 +275,14 @@ static bool create_exportable_image( }; if(vk->vkAllocateMemory(dev, &mem_alloc_info, NULL, out_memory) != VK_SUCCESS) { - fprintf(stderr, "gsr error: create_exportable_image: vkAllocateMemory failed\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "create_exportable_image: vkAllocateMemory failed"); vk->vkDestroyImage(dev, *out_image, NULL); *out_image = VK_NULL_HANDLE; return false; } if(vk->vkBindImageMemory(dev, *out_image, *out_memory, 0) != VK_SUCCESS) { - fprintf(stderr, "gsr error: create_exportable_image: vkBindImageMemory failed\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "create_exportable_image: vkBindImageMemory failed"); vk->vkFreeMemory(dev, *out_memory, NULL); vk->vkDestroyImage(dev, *out_image, NULL); *out_memory = VK_NULL_HANDLE; @@ -285,7 +297,7 @@ static bool create_exportable_image( static bool gsr_video_encoder_vulkan_setup_textures(gsr_video_encoder_vulkan *self, AVCodecContext *video_codec_context, AVFrame *frame) { const int res = av_hwframe_get_buffer(video_codec_context->hw_frames_ctx, frame, 0); if(res < 0) { - fprintf(stderr, "gsr error: gsr_video_encoder_vulkan_setup_textures: av_hwframe_get_buffer failed: %d\n", res); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_video_encoder_vulkan_setup_textures: av_hwframe_get_buffer failed: %d", res); return false; } @@ -293,7 +305,7 @@ static bool gsr_video_encoder_vulkan_setup_textures(gsr_video_encoder_vulkan *se AVVulkanDeviceContext *vv = video_codec_context_get_vulkan_data(video_codec_context); if(!vv) { - fprintf(stderr, "gsr error: gsr_video_encoder_vulkan_setup_textures: failed to get vulkan device context\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_video_encoder_vulkan_setup_textures: failed to get vulkan device context"); return false; } @@ -327,7 +339,7 @@ static bool gsr_video_encoder_vulkan_setup_textures(gsr_video_encoder_vulkan *se }; int fd = -1; if(self->vk.vkGetMemoryFdKHR(vv->act_dev, &fd_info, &fd) != VK_SUCCESS) { - fprintf(stderr, "gsr error: gsr_video_encoder_vulkan_setup_textures: vkGetMemoryFdKHR failed for plane %d\n", i); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_video_encoder_vulkan_setup_textures: vkGetMemoryFdKHR failed for plane %d", i); return false; } @@ -338,7 +350,7 @@ static bool gsr_video_encoder_vulkan_setup_textures(gsr_video_encoder_vulkan *se self->params.egl->glImportMemoryFdEXT(self->gl_memory_objects[i], self->export_memory_size[i], GL_HANDLE_TYPE_OPAQUE_FD_EXT, fd); if(!self->params.egl->glIsMemoryObjectEXT(self->gl_memory_objects[i])) { - fprintf(stderr, "gsr error: gsr_video_encoder_vulkan_setup_textures: failed to import memory FD for plane %d\n", i); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_video_encoder_vulkan_setup_textures: failed to import memory FD for plane %d", i); return false; } } @@ -370,7 +382,7 @@ static bool gsr_video_encoder_vulkan_setup_textures(gsr_video_encoder_vulkan *se .queueFamilyIndex = (uint32_t)get_graphics_queue_family(vv), }; if(self->vk.vkCreateCommandPool(vv->act_dev, &pool_info, NULL, &self->command_pool) != VK_SUCCESS) { - fprintf(stderr, "gsr error: gsr_video_encoder_vulkan_setup_textures: vkCreateCommandPool failed\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_video_encoder_vulkan_setup_textures: vkCreateCommandPool failed"); return false; } @@ -381,7 +393,7 @@ static bool gsr_video_encoder_vulkan_setup_textures(gsr_video_encoder_vulkan *se .commandBufferCount = 1, }; if(self->vk.vkAllocateCommandBuffers(vv->act_dev, &cb_alloc_info, &self->command_buffer) != VK_SUCCESS) { - fprintf(stderr, "gsr error: gsr_video_encoder_vulkan_setup_textures: vkAllocateCommandBuffers failed\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_video_encoder_vulkan_setup_textures: vkAllocateCommandBuffers failed"); return false; } @@ -389,11 +401,26 @@ static bool gsr_video_encoder_vulkan_setup_textures(gsr_video_encoder_vulkan *se .sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO, }; if(self->vk.vkCreateFence(vv->act_dev, &fence_info, NULL, &self->fence) != VK_SUCCESS) { - fprintf(stderr, "gsr error: gsr_video_encoder_vulkan_setup_textures: vkCreateFence failed\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_video_encoder_vulkan_setup_textures: vkCreateFence failed"); return false; } - self->vk.vkGetDeviceQueue(vv->act_dev, (uint32_t)get_graphics_queue_family(vv), 0, &self->vk_queue); + /* + The queues have to be retrieved with vkGetDeviceQueue2 because vkGetDeviceQueue only works for queues that + were created without flags, and FFmpeg creates them with VK_DEVICE_QUEUE_CREATE_INTERNALLY_SYNCHRONIZED_BIT_KHR + when the driver supports it. vkGetDeviceQueue returns a NULL queue in that case, which crashes when it's used. + */ + VkDeviceQueueInfo2 queue_info = { + .sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_INFO_2, + .flags = get_device_queue_create_flags(vv), + .queueFamilyIndex = (uint32_t)get_graphics_queue_family(vv), + .queueIndex = 0, + }; + self->vk.vkGetDeviceQueue2(vv->act_dev, &queue_info, &self->vk_queue); + if(!self->vk_queue) { + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_video_encoder_vulkan_setup_textures: vkGetDeviceQueue2 failed"); + return false; + } /* Transition export images UNDEFINED → GENERAL so GL can use them */ VkCommandBufferBeginInfo begin_info = { @@ -484,7 +511,7 @@ static bool gsr_video_encoder_vulkan_start(gsr_video_encoder *encoder, AVCodecCo } if(FFALIGN(video_codec_context->width, 2) != FFALIGN(frame->width, 2) || FFALIGN(video_codec_context->height, 2) != FFALIGN(frame->height, 2)) { - fprintf(stderr, "gsr warning: gsr_video_encoder_vulkan_start: black bars have been added to the video because of a bug in AMD drivers/hardware. Record with h264/hevc vulkan codec instead (-k h264_vulkan) to get around this issue\n"); + gsr_log(GSR_LOG_LEVEL_WARNING, "gsr_video_encoder_vulkan_start: black bars have been added to the video because of a bug in AMD drivers/hardware. Record with h264/hevc vulkan codec instead (-k h264_vulkan) to get around this issue"); } if(video_codec_context->width < 128) @@ -835,7 +862,7 @@ void gsr_video_encoder_vulkan_stop(gsr_video_encoder_vulkan *self, AVCodecContex } } - if(video_codec_context->hw_frames_ctx) + if(video_codec_context && video_codec_context->hw_frames_ctx) av_buffer_unref(&video_codec_context->hw_frames_ctx); if(self->device_ctx) av_buffer_unref(&self->device_ctx); diff --git a/src/ffmpeg_utils.c b/src/ffmpeg_utils.c new file mode 100644 index 0000000..fc4e4ed --- /dev/null +++ b/src/ffmpeg_utils.c @@ -0,0 +1,40 @@ +#include "../include/ffmpeg_utils.h" +#include "../include/log.h" + +#include <string.h> +#include <libavutil/error.h> +#include <libavutil/opt.h> +#include <libavformat/avformat.h> + +static _Thread_local char av_error_buffer[AV_ERROR_MAX_STRING_SIZE]; + +const char* gsr_av_error_to_string(int err) { + if(av_strerror(err, av_error_buffer, sizeof(av_error_buffer)) < 0) + strcpy(av_error_buffer, "Unknown error"); + return av_error_buffer; +} + +void gsr_av_format_context_mark_packet_written(AVFormatContext *av_format_context) { + av_format_context->opaque = (void*)1; +} + +static bool av_format_context_uses_hybrid_fragmented(AVFormatContext *av_format_context) { + if(LIBAVFORMAT_VERSION_INT < AV_VERSION_INT(62, 6, 101)) + return false; + + const AVOption *opt = av_opt_find(av_format_context->priv_data, "movflags", NULL, 0, 0); + if(!opt || !opt->unit) + return false; + + return av_opt_find(av_format_context->priv_data, "hybrid_fragmented", opt->unit, 0, 0) != NULL; +} + +int gsr_av_format_context_write_trailer(AVFormatContext *av_format_context) { + const bool packet_written = av_format_context->opaque != NULL; + if(!packet_written && av_format_context_uses_hybrid_fragmented(av_format_context)) { + gsr_log(GSR_LOG_LEVEL_WARNING, "not finalizing the video file because it has no video/audio data"); + return 0; + } + + return av_write_trailer(av_format_context); +} diff --git a/src/image_writer.c b/src/image_writer.c index adc88a7..117c8b8 100644 --- a/src/image_writer.c +++ b/src/image_writer.c @@ -1,4 +1,5 @@ #include "../include/image_writer.h" +#include "../include/log.h" #include "../include/egl.h" #include "../include/utils.h" @@ -8,7 +9,6 @@ #include <stdlib.h> #include <stdint.h> #include <stdio.h> -#include <assert.h> #include <dlfcn.h> #define TJPF_RGBA 7 @@ -83,7 +83,7 @@ bool gsr_image_writer_init_opengl(gsr_image_writer *self, gsr_egl *egl, int widt self->height = height; self->texture = gl_create_texture(self->egl, self->width, self->height, GL_RGBA8, GL_RGBA, GL_NEAREST); /* TODO: use GL_RGB16 instead of GL_RGB8 for hdr/10-bit */ if(self->texture == 0) { - fprintf(stderr, "gsr error: gsr_image_writer_init: failed to create texture\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_image_writer_init: failed to create texture"); return false; } @@ -128,7 +128,7 @@ static bool gsr_image_writer_write_memory_to_file(gsr_image_writer *self, const } if(!success) - fprintf(stderr, "gsr error: gsr_image_writer_write_to_file: failed to write image data to output file %s\n", filepath); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_image_writer_write_to_file: failed to write image data to output file %s", filepath); return success; } @@ -136,7 +136,7 @@ static bool gsr_image_writer_write_memory_to_file(gsr_image_writer *self, const static bool gsr_image_writer_write_opengl_texture_to_file(gsr_image_writer *self, const char *filepath, gsr_image_format image_format, int quality) { uint8_t *frame_data = malloc(self->width * self->height * 4); if(!frame_data) { - fprintf(stderr, "gsr error: gsr_image_writer_write_to_file: failed to allocate memory for image frame\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_image_writer_write_to_file: failed to allocate memory for image frame"); return false; } diff --git a/src/json.c b/src/json.c new file mode 100644 index 0000000..f7d1cd7 --- /dev/null +++ b/src/json.c @@ -0,0 +1,69 @@ +#include "../include/json.h" + +#define SJ_IMPL +#include "../external/sj.h" + +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <errno.h> + +bool gsr_json_string_equals(const sj_Value *value, const char *str) { + const size_t value_size = value->end - value->start; + return strlen(str) == value_size && memcmp(value->start, str, value_size) == 0; +} + +bool gsr_json_number_to_int64(const sj_Value *value, int64_t *result) { + if(value->type != SJ_NUMBER) + return false; + + char buffer[32]; + const size_t value_size = value->end - value->start; + if(value_size == 0 || value_size >= sizeof(buffer)) + return false; + + memcpy(buffer, value->start, value_size); + buffer[value_size] = '\0'; + + char *number_end = NULL; + errno = 0; + const long long parsed_value = strtoll(buffer, &number_end, 10); + if(errno != 0 || number_end != buffer + value_size) + return false; + + *result = parsed_value; + return true; +} + +void gsr_json_escape_string(char *buffer, size_t buffer_size, const char *str) { + char escape_buffer[8]; + size_t offset = 0; + buffer[0] = '\0'; + + for(size_t i = 0; str[i] != '\0'; ++i) { + const unsigned char c = str[i]; + const char *escaped = escape_buffer; + switch(c) { + case '"': escaped = "\\\""; break; + case '\\': escaped = "\\\\"; break; + case '\n': escaped = "\\n"; break; + case '\r': escaped = "\\r"; break; + case '\t': escaped = "\\t"; break; + default: { + if(c < 0x20) + snprintf(escape_buffer, sizeof(escape_buffer), "\\u%04x", c); + else + snprintf(escape_buffer, sizeof(escape_buffer), "%c", c); + break; + } + } + + const size_t escaped_size = strlen(escaped); + if(offset + escaped_size >= buffer_size) + break; + + memcpy(buffer + offset, escaped, escaped_size); + offset += escaped_size; + buffer[offset] = '\0'; + } +} diff --git a/src/kde_night_light.c b/src/kde_night_light.c index d5c9578..d3491ef 100644 --- a/src/kde_night_light.c +++ b/src/kde_night_light.c @@ -1,8 +1,8 @@ #include "../include/kde_night_light.h" +#include "../include/log.h" #ifdef GSR_DBUS -#include <stdio.h> #include <stdlib.h> #include <string.h> #include <math.h> @@ -342,11 +342,10 @@ bool gsr_kde_night_light_get_inverse_matrix(gsr_kde_night_light *self, gsr_night #else /* GSR_DBUS */ -#include <stdio.h> #include <stddef.h> gsr_kde_night_light* gsr_kde_night_light_create(void) { - fprintf(stderr, "gsr warning: kde night light handling disabled because gsr was compiled without pipewire (which also disables dbus)\n"); + gsr_log(GSR_LOG_LEVEL_WARNING, "kde night light handling disabled because gsr was compiled without pipewire (which also disables dbus)"); return NULL; } diff --git a/src/library_loader.c b/src/library_loader.c index 0aeee9b..8b2e8a1 100644 --- a/src/library_loader.c +++ b/src/library_loader.c @@ -1,8 +1,8 @@ #include "../include/library_loader.h" +#include "../include/log.h" #include <dlfcn.h> #include <stdbool.h> -#include <stdio.h> void* dlsym_print_fail(void *handle, const char *name, bool required) { dlerror(); @@ -10,7 +10,7 @@ void* dlsym_print_fail(void *handle, const char *name, bool required) { char *err_str = dlerror(); if(!sym) - fprintf(stderr, "%s: dlsym(handle, \"%s\") failed, error: %s\n", required ? "error" : "warning", name, err_str ? err_str : "(null)"); + gsr_log(required ? GSR_LOG_LEVEL_ERROR : GSR_LOG_LEVEL_WARNING, "dlsym(handle, \"%s\") failed, error: %s", name, err_str ? err_str : "(null)"); return sym; } diff --git a/src/log.c b/src/log.c new file mode 100644 index 0000000..b3ebc49 --- /dev/null +++ b/src/log.c @@ -0,0 +1,47 @@ +#include "../include/log.h" + +#include <stdio.h> +#include <stdarg.h> + +#define GSR_LOG_MESSAGE_MAX_SIZE 4096 + +static const char* log_level_to_string(gsr_log_level level) { + switch(level) { + case GSR_LOG_LEVEL_DEBUG: return "debug"; + case GSR_LOG_LEVEL_INFO: return "info"; + case GSR_LOG_LEVEL_WARNING: return "warning"; + case GSR_LOG_LEVEL_ERROR: return "error"; + } + return "unknown"; +} + +static void gsr_log_default_handler(gsr_log_level level, const char *message, void *userdata) { + (void)userdata; + fprintf(stderr, "gsr %s: %s\n", log_level_to_string(level), message); +} + +static gsr_log_level log_level = GSR_LOG_LEVEL_INFO; +static gsr_log_handler log_handler = gsr_log_default_handler; +static void *log_handler_userdata = NULL; + +void gsr_log(gsr_log_level level, const char *fmt, ...) { + if(level < log_level) + return; + + char message[GSR_LOG_MESSAGE_MAX_SIZE]; + va_list args; + va_start(args, fmt); + vsnprintf(message, sizeof(message), fmt, args); + va_end(args); + + log_handler(level, message, log_handler_userdata); +} + +void gsr_log_set_level(gsr_log_level level) { + log_level = level; +} + +void gsr_log_set_handler(gsr_log_handler handler, void *userdata) { + log_handler = handler ? handler : gsr_log_default_handler; + log_handler_userdata = userdata; +} diff --git a/src/main.cpp b/src/main.cpp deleted file mode 100644 index 06996c3..0000000 --- a/src/main.cpp +++ /dev/null @@ -1,4734 +0,0 @@ -extern "C" { -#include "../include/capture/nvfbc.h" -#include "../include/capture/xcomposite.h" -#include "../include/capture/ximage.h" -#include "../include/capture/kms.h" -#include "../include/capture/v4l2.h" -#ifdef GSR_PORTAL -#include "../include/capture/portal.h" -#include "../include/dbus.h" -#endif -#ifdef GSR_APP_AUDIO -#include "../include/pipewire_audio.h" -#endif -#include "../include/encoder/encoder.h" -#include "../include/encoder/video/nvenc.h" -#include "../include/encoder/video/vaapi.h" -#include "../include/encoder/video/vulkan.h" -#include "../include/encoder/video/software.h" -#include "../include/codec_query/nvenc.h" -#include "../include/codec_query/vaapi.h" -#include "../include/codec_query/vulkan.h" -#include "../include/window/x11.h" -#include "../include/window/wayland.h" -#include "../include/egl.h" -#include "../include/utils.h" -#include "../include/damage.h" -#include "../include/color_conversion.h" -#include "../include/image_writer.h" -#include "../include/args_parser.h" -#include "../include/plugins.h" -#include "../kms/client/kms_client.h" -} - -#include <assert.h> -#include <stdio.h> -#include <stdlib.h> -#include <string> -#include <thread> -#include <mutex> -#include <signal.h> -#include <sys/stat.h> -#include <unistd.h> -#include <sys/wait.h> -#include <inttypes.h> -#include <libgen.h> -#include <malloc.h> - -#include "../include/sound.hpp" - -extern "C" { -#include <libavutil/pixfmt.h> -#include <libavcodec/avcodec.h> -#include <libavformat/avformat.h> -#include <libavutil/opt.h> -#include <libswresample/swresample.h> -#include <libavutil/avutil.h> -#include <libavutil/time.h> -#include <libavutil/mastering_display_metadata.h> -#include <libavfilter/avfilter.h> -#include <libavfilter/buffersink.h> -#include <libavfilter/buffersrc.h> -} - -#include <future> - -#ifndef GSR_VERSION -#define GSR_VERSION "unknown" -#endif - -// TODO: If options are not supported then they are returned (allocated) in the options. This should be free'd. - -// TODO: Remove LIBAVUTIL_VERSION_MAJOR checks in the future when ubuntu, pop os LTS etc update ffmpeg to >= 5.0 - -static const int AUDIO_SAMPLE_RATE = 48000; - -static const int VIDEO_STREAM_INDEX = 0; - -static thread_local char av_error_buffer[AV_ERROR_MAX_STRING_SIZE]; - -typedef struct { - const gsr_window *window; -} MonitorOutputCallbackUserdata; - -static void monitor_output_callback_print(const gsr_monitor *monitor, void *userdata) { - const MonitorOutputCallbackUserdata *options = (MonitorOutputCallbackUserdata*)userdata; - vec2i monitor_position = monitor->pos; - vec2i monitor_size = monitor->size; - if(gsr_window_get_display_server(options->window) == GSR_DISPLAY_SERVER_WAYLAND) { - gsr_monitor_rotation monitor_rotation = GSR_MONITOR_ROT_0; - drm_monitor_get_display_server_data(options->window, monitor, &monitor_rotation, &monitor_position); - if(monitor_rotation == GSR_MONITOR_ROT_90 || monitor_rotation == GSR_MONITOR_ROT_270) - std::swap(monitor_size.x, monitor_size.y); - } - fprintf(stderr, " \"%.*s\" (%dx%d+%d+%d)\n", monitor->name_len, monitor->name, monitor_size.x, monitor_size.y, monitor_position.x, monitor_position.y); -} - -typedef struct { - char *output_name; -} FirstOutputCallback; - -static void get_first_output_callback(const gsr_monitor *monitor, void *userdata) { - FirstOutputCallback *data = (FirstOutputCallback*)userdata; - if(!data->output_name) - data->output_name = strdup(monitor->name); -} - -typedef struct { - gsr_window *window; - vec2i position; - char *output_name; - vec2i monitor_pos; - vec2i monitor_size; - double monitor_scale_inverted; -} MonitorByPositionCallback; - -static void get_monitor_by_position_callback(const gsr_monitor *monitor, void *userdata) { - MonitorByPositionCallback *data = (MonitorByPositionCallback*)userdata; - - const vec2i monitor_position = monitor->logical_pos; - const vec2i monitor_size = monitor->size; - const vec2i monitor_logical_size = monitor->logical_size; - - if(!data->output_name && data->position.x >= monitor_position.x && data->position.x <= monitor_position.x + monitor_logical_size.x - && data->position.y >= monitor_position.y && data->position.y <= monitor_position.y + monitor_logical_size.y) - { - data->output_name = strdup(monitor->name); - data->monitor_pos = monitor_position; - data->monitor_size = monitor_size; - data->monitor_scale_inverted = (double)monitor_size.x / (double)monitor_logical_size.x; - } -} - -static char* av_error_to_string(int err) { - if(av_strerror(err, av_error_buffer, sizeof(av_error_buffer)) < 0) - strcpy(av_error_buffer, "Unknown error"); - return av_error_buffer; -} - -static int x11_error_handler(Display*, XErrorEvent*) { - return 0; -} - -static int x11_io_error_handler(Display*) { - return 0; -} - -static AVCodecID audio_codec_get_id(gsr_audio_codec audio_codec) { - switch(audio_codec) { - case GSR_AUDIO_CODEC_AAC: return AV_CODEC_ID_AAC; - case GSR_AUDIO_CODEC_OPUS: return AV_CODEC_ID_OPUS; - case GSR_AUDIO_CODEC_FLAC: return AV_CODEC_ID_FLAC; - } - assert(false); - return AV_CODEC_ID_AAC; -} - -static AVSampleFormat audio_codec_get_sample_format(AVCodecContext *audio_codec_context, gsr_audio_codec audio_codec, const AVCodec *codec, bool mix_audio) { - (void)audio_codec_context; - switch(audio_codec) { - case GSR_AUDIO_CODEC_AAC: { - return AV_SAMPLE_FMT_FLTP; - } - case GSR_AUDIO_CODEC_OPUS: { - bool supports_s16 = false; - bool supports_flt = false; - - #if LIBAVCODEC_VERSION_INT < AV_VERSION_INT(61, 15, 0) - for(size_t i = 0; codec->sample_fmts && codec->sample_fmts[i] != AV_SAMPLE_FMT_NONE; ++i) { - if(codec->sample_fmts[i] == AV_SAMPLE_FMT_S16) { - supports_s16 = true; - } else if(codec->sample_fmts[i] == AV_SAMPLE_FMT_FLT) { - supports_flt = true; - } - } - #else - const enum AVSampleFormat *sample_fmts = NULL; - if(avcodec_get_supported_config(audio_codec_context, codec, AV_CODEC_CONFIG_SAMPLE_FORMAT, 0, (const void**)&sample_fmts, NULL) >= 0) { - if(sample_fmts) { - for(size_t i = 0; sample_fmts[i] != AV_SAMPLE_FMT_NONE; ++i) { - if(sample_fmts[i] == AV_SAMPLE_FMT_S16) { - supports_s16 = true; - } else if(sample_fmts[i] == AV_SAMPLE_FMT_FLT) { - supports_flt = true; - } - } - } else { - // What a dumb API. It returns NULL if all formats are supported - supports_s16 = true; - supports_flt = true; - } - } - #endif - - // Amix only works with float audio - if(mix_audio) - supports_s16 = false; - - if(!supports_s16 && !supports_flt) { - fprintf(stderr, "gsr warning: opus audio codec is chosen but your ffmpeg version does not support s16/flt sample format and performance might be slightly worse.\n"); - fprintf(stderr, " You can either rebuild ffmpeg with libopus instead of the built-in opus, use the flatpak version of gpu screen recorder or record with aac audio codec instead (-ac aac).\n"); - fprintf(stderr, " Falling back to fltp audio sample format instead.\n"); - } - - if(supports_s16) - return AV_SAMPLE_FMT_S16; - else if(supports_flt) - return AV_SAMPLE_FMT_FLT; - else - return AV_SAMPLE_FMT_FLTP; - } - case GSR_AUDIO_CODEC_FLAC: { - return AV_SAMPLE_FMT_S32; - } - } - assert(false); - return AV_SAMPLE_FMT_FLTP; -} - -static int64_t audio_codec_get_get_bitrate(gsr_audio_codec audio_codec) { - switch(audio_codec) { - case GSR_AUDIO_CODEC_AAC: return 160000; - case GSR_AUDIO_CODEC_OPUS: return 128000; - case GSR_AUDIO_CODEC_FLAC: return 128000; - } - assert(false); - return 128000; -} - -static AudioFormat audio_codec_context_get_audio_format(const AVCodecContext *audio_codec_context) { - switch(audio_codec_context->sample_fmt) { - case AV_SAMPLE_FMT_FLT: return F32; - case AV_SAMPLE_FMT_FLTP: return S32; - case AV_SAMPLE_FMT_S16: return S16; - case AV_SAMPLE_FMT_S32: return S32; - default: return S16; - } -} - -static AVSampleFormat audio_format_to_sample_format(const AudioFormat audio_format) { - switch(audio_format) { - case S16: return AV_SAMPLE_FMT_S16; - case S32: return AV_SAMPLE_FMT_S32; - case F32: return AV_SAMPLE_FMT_FLT; - } - assert(false); - return AV_SAMPLE_FMT_S16; -} - -static AVCodecContext* create_audio_codec_context(int fps, gsr_audio_codec audio_codec, bool mix_audio, int64_t audio_bitrate) { - (void)fps; - const AVCodec *codec = avcodec_find_encoder(audio_codec_get_id(audio_codec)); - if (!codec) { - fprintf(stderr, "gsr error: Could not find %s audio encoder\n", audio_codec_get_name(audio_codec)); - _exit(1); - } - - AVCodecContext *codec_context = avcodec_alloc_context3(codec); - - assert(codec->type == AVMEDIA_TYPE_AUDIO); - codec_context->codec_id = codec->id; - codec_context->sample_fmt = audio_codec_get_sample_format(codec_context, audio_codec, codec, mix_audio); - codec_context->bit_rate = audio_bitrate == 0 ? audio_codec_get_get_bitrate(audio_codec) : audio_bitrate; - codec_context->sample_rate = AUDIO_SAMPLE_RATE; - if(audio_codec == GSR_AUDIO_CODEC_AAC) { -#if LIBAVCODEC_VERSION_MAJOR < 62 - codec_context->profile = FF_PROFILE_AAC_LOW; -#else - codec_context->profile = AV_PROFILE_AAC_LOW; -#endif - } -#if LIBAVCODEC_VERSION_MAJOR < 60 - codec_context->channel_layout = AV_CH_LAYOUT_STEREO; - codec_context->channels = 2; -#else - av_channel_layout_default(&codec_context->ch_layout, 2); -#endif - - codec_context->time_base.num = 1; - codec_context->time_base.den = codec_context->sample_rate; - codec_context->thread_count = 1; - codec_context->flags |= AV_CODEC_FLAG_GLOBAL_HEADER; - - return codec_context; -} - -static int video_quality_to_h264_equivalent_qp(gsr_video_quality video_quality) { - switch(video_quality) { - case GSR_VIDEO_QUALITY_MEDIUM: return 35; - case GSR_VIDEO_QUALITY_HIGH: return 30; - case GSR_VIDEO_QUALITY_VERY_HIGH: return 25; - case GSR_VIDEO_QUALITY_ULTRA: return 22; - } - return 22; -} - -/* AV1/VP9 use the 0-255 qindex quantizer scale and VP8 uses 0-127. Unlike h264 qp the quantizer step size - is not exponential in qindex, so the mapping to an equivalent h264 qp is not linear in the quantizer range. - qindex = h264 qp * 4 matches the qindex = crf * 4 scale used by libvpx/libaom, where crf is roughly equivalent to h264 qp. */ -static int video_quality_to_codec_quality_value(AVCodecID codec_id, gsr_video_quality video_quality) { - const int h264_qp = video_quality_to_h264_equivalent_qp(video_quality); - switch(codec_id) { - case AV_CODEC_ID_H264: - case AV_CODEC_ID_HEVC: - return h264_qp; - case AV_CODEC_ID_AV1: - case AV_CODEC_ID_VP9: - return h264_qp * 4; - case AV_CODEC_ID_VP8: - return h264_qp * 2; - default: - return h264_qp; - } -} - -static int vbr_get_quality_parameter(AVCodecContext *codec_context, gsr_video_quality video_quality, bool hdr) { - // 8 bit / 10 bit = 80% - const float qp_multiply = hdr ? 8.0f/10.0f : 1.0f; - return video_quality_to_codec_quality_value(codec_context->codec_id, video_quality) * qp_multiply; -} - -static AVCodecContext *create_video_codec_context(AVPixelFormat pix_fmt, const AVCodec *codec, const gsr_egl &egl, const args_parser &arg_parser, int width, int height) { - const bool use_software_video_encoder = arg_parser.video_encoder == GSR_VIDEO_ENCODER_HW_CPU; - const bool hdr = video_codec_is_hdr(arg_parser.video_codec); - AVCodecContext *codec_context = avcodec_alloc_context3(codec); - - //double fps_ratio = (double)fps / 30.0; - - assert(codec->type == AVMEDIA_TYPE_VIDEO); - codec_context->codec_id = codec->id; - codec_context->width = width; - codec_context->height = height; - // Timebase: This is the fundamental unit of time (in seconds) in terms - // of which frame timestamps are represented. For fixed-fps content, - // timebase should be 1/framerate and timestamp increments should be - // identical to 1 - codec_context->time_base.num = 1; - codec_context->time_base.den = arg_parser.framerate_mode == GSR_FRAMERATE_MODE_CONSTANT ? arg_parser.fps : AV_TIME_BASE; - codec_context->framerate.num = arg_parser.fps; - codec_context->framerate.den = 1; - codec_context->sample_aspect_ratio.num = 0; - codec_context->sample_aspect_ratio.den = 0; - if(arg_parser.low_latency_recording) { - codec_context->flags |= (AV_CODEC_FLAG_CLOSED_GOP | AV_CODEC_FLAG_LOW_DELAY); - codec_context->flags2 |= AV_CODEC_FLAG2_FAST; - //codec_context->gop_size = std::numeric_limits<int>::max(); - //codec_context->keyint_min = std::numeric_limits<int>::max(); - codec_context->gop_size = arg_parser.fps * arg_parser.keyint; - } else { - // High values reduce file size but increases time it takes to seek - codec_context->gop_size = arg_parser.fps * arg_parser.keyint; - } - codec_context->max_b_frames = 0; - codec_context->pix_fmt = pix_fmt; - codec_context->color_range = arg_parser.color_range == GSR_COLOR_RANGE_LIMITED ? AVCOL_RANGE_MPEG : AVCOL_RANGE_JPEG; - if(hdr) { - codec_context->color_primaries = AVCOL_PRI_BT2020; - codec_context->color_trc = AVCOL_TRC_SMPTE2084; - codec_context->colorspace = AVCOL_SPC_BT2020_NCL; - } else { - codec_context->color_primaries = AVCOL_PRI_BT709; - codec_context->color_trc = AVCOL_TRC_BT709; - codec_context->colorspace = AVCOL_SPC_BT709; - } - //codec_context->chroma_sample_location = AVCHROMA_LOC_CENTER; - // Can't use this because it's fucking broken in ffmpeg 8 or new mesa. It produces garbage output - //if(codec->id == AV_CODEC_ID_HEVC) - // codec_context->codec_tag = MKTAG('h', 'v', 'c', '1'); // QuickTime on MacOS requires this or the video wont be playable - - if(arg_parser.bitrate_mode == GSR_BITRATE_MODE_CBR) { - codec_context->bit_rate = arg_parser.video_bitrate; - codec_context->rc_max_rate = codec_context->bit_rate; - //codec_context->rc_min_rate = codec_context->bit_rate; - codec_context->rc_buffer_size = codec_context->bit_rate;//codec_context->bit_rate / 10; - codec_context->rc_initial_buffer_occupancy = 0;//codec_context->bit_rate;//codec_context->bit_rate * 1000; - } else if(arg_parser.bitrate_mode == GSR_BITRATE_MODE_VBR) { - const int quality = vbr_get_quality_parameter(codec_context, arg_parser.video_quality, hdr); - switch(arg_parser.video_quality) { - case GSR_VIDEO_QUALITY_MEDIUM: - codec_context->qmin = quality; - codec_context->qmax = quality; - codec_context->bit_rate = 100000;//4500000 + (codec_context->width * codec_context->height)*0.75; - break; - case GSR_VIDEO_QUALITY_HIGH: - codec_context->qmin = quality; - codec_context->qmax = quality; - codec_context->bit_rate = 100000;//10000000-9000000 + (codec_context->width * codec_context->height)*0.75; - break; - case GSR_VIDEO_QUALITY_VERY_HIGH: - codec_context->qmin = quality; - codec_context->qmax = quality; - codec_context->bit_rate = 100000;//10000000-9000000 + (codec_context->width * codec_context->height)*0.75; - break; - case GSR_VIDEO_QUALITY_ULTRA: - codec_context->qmin = quality; - codec_context->qmax = quality; - codec_context->bit_rate = 100000;//10000000-9000000 + (codec_context->width * codec_context->height)*0.75; - break; - } - - codec_context->rc_max_rate = codec_context->bit_rate; - //codec_context->rc_min_rate = codec_context->bit_rate; - codec_context->rc_buffer_size = codec_context->bit_rate;//codec_context->bit_rate / 10; - codec_context->rc_initial_buffer_occupancy = codec_context->bit_rate;//codec_context->bit_rate * 1000; - } else { - //codec_context->rc_buffer_size = 50000 * 1000; - } - //codec_context->profile = FF_PROFILE_H264_MAIN; - if (codec_context->codec_id == AV_CODEC_ID_MPEG1VIDEO) - codec_context->mb_decision = 2; - - const bool uses_vaapi_encoder = !use_software_video_encoder && egl.gpu_info.vendor != GSR_GPU_VENDOR_NVIDIA && !video_codec_is_vulkan(arg_parser.video_codec); - if(uses_vaapi_encoder && arg_parser.bitrate_mode != GSR_BITRATE_MODE_CBR) { - // 8 bit / 10 bit = 80%, and increase it even more - const float quality_multiply = hdr ? (8.0f/10.0f * 0.7f) : 1.0f; - codec_context->global_quality = video_quality_to_codec_quality_value(codec_context->codec_id, arg_parser.video_quality) * quality_multiply; - } - - av_opt_set_int(codec_context->priv_data, "b_ref_mode", 0, 0); - //av_opt_set_int(codec_context->priv_data, "cbr", true, 0); - - if(egl.gpu_info.vendor != GSR_GPU_VENDOR_NVIDIA || video_codec_is_vulkan(arg_parser.video_codec)) { - // TODO: More options, better options - //codec_context->bit_rate = codec_context->width * codec_context->height; - switch(arg_parser.bitrate_mode) { - case GSR_BITRATE_MODE_QP: { - if(video_codec_is_vulkan(arg_parser.video_codec)) - av_opt_set(codec_context->priv_data, "rc_mode", "cqp", 0); - else if(egl.gpu_info.vendor == GSR_GPU_VENDOR_NVIDIA) - av_opt_set(codec_context->priv_data, "rc", "constqp", 0); - else - av_opt_set(codec_context->priv_data, "rc_mode", "CQP", 0); - break; - } - case GSR_BITRATE_MODE_VBR: { - if(video_codec_is_vulkan(arg_parser.video_codec)) - av_opt_set(codec_context->priv_data, "rc_mode", "vbr", 0); - else if(egl.gpu_info.vendor == GSR_GPU_VENDOR_NVIDIA) - av_opt_set(codec_context->priv_data, "rc", "vbr", 0); - else - av_opt_set(codec_context->priv_data, "rc_mode", "VBR", 0); - break; - } - case GSR_BITRATE_MODE_CBR: { - if(video_codec_is_vulkan(arg_parser.video_codec)) - av_opt_set(codec_context->priv_data, "rc_mode", "cbr", 0); - else if(egl.gpu_info.vendor == GSR_GPU_VENDOR_NVIDIA) - av_opt_set(codec_context->priv_data, "rc", "cbr", 0); - else - av_opt_set(codec_context->priv_data, "rc_mode", "CBR", 0); - break; - } - } - //codec_context->global_quality = 4; - //codec_context->compression_level = 2; - } - - //av_opt_set(codec_context->priv_data, "bsf", "hevc_metadata=colour_primaries=9:transfer_characteristics=16:matrix_coefficients=9", 0); - - if(arg_parser.tune == GSR_TUNE_QUALITY) - codec_context->max_b_frames = 2; - - codec_context->flags |= AV_CODEC_FLAG_GLOBAL_HEADER; - - return codec_context; -} - -static void open_audio(AVCodecContext *audio_codec_context, const char *ffmpeg_audio_opts) { - AVDictionary *options = nullptr; - av_dict_set(&options, "strict", "experimental", 0); - - if(ffmpeg_audio_opts) - av_dict_parse_string(&options, ffmpeg_audio_opts, "=", ";", 0); - - int ret; - ret = avcodec_open2(audio_codec_context, audio_codec_context->codec, &options); - if(ret < 0) { - fprintf(stderr, "failed to open codec, reason: %s\n", av_error_to_string(ret)); - _exit(1); - } -} - -static AVFrame* create_audio_frame(AVCodecContext *audio_codec_context) { - AVFrame *frame = av_frame_alloc(); - if(!frame) { - fprintf(stderr, "failed to allocate audio frame\n"); - _exit(1); - } - - frame->sample_rate = audio_codec_context->sample_rate; - frame->nb_samples = audio_codec_context->frame_size; - frame->format = audio_codec_context->sample_fmt; -#if LIBAVCODEC_VERSION_MAJOR < 60 - frame->channels = audio_codec_context->channels; - frame->channel_layout = audio_codec_context->channel_layout; -#else - av_channel_layout_copy(&frame->ch_layout, &audio_codec_context->ch_layout); -#endif - - int ret = av_frame_get_buffer(frame, 0); - if(ret < 0) { - fprintf(stderr, "failed to allocate audio data buffers, reason: %s\n", av_error_to_string(ret)); - _exit(1); - } - - return frame; -} - -static void dict_set_profile(AVCodecContext *codec_context, gsr_gpu_vendor vendor, gsr_color_depth color_depth, gsr_video_codec video_codec, AVDictionary **options) { - #if LIBAVCODEC_VERSION_INT < AV_VERSION_INT(61, 17, 100) - if(codec_context->codec_id == AV_CODEC_ID_H264) { - // TODO: Only for vaapi - //if(color_depth == GSR_COLOR_DEPTH_10_BITS) - // av_dict_set(options, "profile", "high10", 0); - //else - av_dict_set(options, "profile", "high", 0); - } else if(codec_context->codec_id == AV_CODEC_ID_AV1) { - if(vendor == GSR_GPU_VENDOR_NVIDIA) { - if(color_depth == GSR_COLOR_DEPTH_10_BITS) - av_dict_set_int(options, "highbitdepth", 1, 0); - } else { - av_dict_set(options, "profile", "main", 0); // TODO: use professional instead? - } - } else if(codec_context->codec_id == AV_CODEC_ID_HEVC) { - if(color_depth == GSR_COLOR_DEPTH_10_BITS) - av_dict_set(options, "profile", "main10", 0); - else - av_dict_set(options, "profile", "main", 0); - } - #else - const bool use_nvidia_values = vendor == GSR_GPU_VENDOR_NVIDIA && !video_codec_is_vulkan(video_codec); - if(codec_context->codec_id == AV_CODEC_ID_H264) { - // TODO: Only for vaapi - //if(color_depth == GSR_COLOR_DEPTH_10_BITS) - // av_dict_set_int(options, "profile", AV_PROFILE_H264_HIGH_10, 0); - //else - av_dict_set_int(options, "profile", use_nvidia_values ? 2 : AV_PROFILE_H264_HIGH, 0); - } else if(codec_context->codec_id == AV_CODEC_ID_AV1) { - if(use_nvidia_values) { - if(color_depth == GSR_COLOR_DEPTH_10_BITS) - av_dict_set_int(options, "highbitdepth", 1, 0); - } else { - av_dict_set_int(options, "profile", AV_PROFILE_AV1_MAIN, 0); // TODO: use professional instead? - } - } else if(codec_context->codec_id == AV_CODEC_ID_HEVC) { - if(color_depth == GSR_COLOR_DEPTH_10_BITS) - av_dict_set_int(options, "profile", use_nvidia_values ? 1 : AV_PROFILE_HEVC_MAIN_10, 0); - else - av_dict_set_int(options, "profile", use_nvidia_values ? 0 : AV_PROFILE_HEVC_MAIN, 0); - } - #endif -} - -static void video_software_set_qp(AVCodecContext *codec_context, gsr_video_quality video_quality, bool hdr, AVDictionary **options) { - // 8 bit / 10 bit = 80% - const float qp_multiply = hdr ? 8.0f/10.0f : 1.0f; - av_dict_set_int(options, "qp", video_quality_to_codec_quality_value(codec_context->codec_id, video_quality) * qp_multiply, 0); -} - -static void open_video_software(AVCodecContext *codec_context, const args_parser &arg_parser) { - const bool hdr = video_codec_is_hdr(arg_parser.video_codec); - AVDictionary *options = nullptr; - - if(arg_parser.bitrate_mode == GSR_BITRATE_MODE_QP) - video_software_set_qp(codec_context, arg_parser.video_quality, hdr, &options); - - av_dict_set(&options, "preset", "veryfast", 0); - av_dict_set(&options, "tune", "film", 0); - av_dict_set_int(&options, "forced-idr", 1, 0); - - if(codec_context->codec_id == AV_CODEC_ID_H264) { - av_dict_set(&options, "coder", "cabac", 0); // TODO: cavlc is faster than cabac but worse compression. Which to use? - } - - av_dict_set(&options, "strict", "experimental", 0); - - if(arg_parser.ffmpeg_video_opts) - av_dict_parse_string(&options, arg_parser.ffmpeg_video_opts, "=", ";", 0); - - int ret = avcodec_open2(codec_context, codec_context->codec, &options); - if (ret < 0) { - fprintf(stderr, "gsr error: Could not open video codec: %s\n", av_error_to_string(ret)); - _exit(1); - } -} - -static void video_set_rc(gsr_video_codec video_codec, gsr_gpu_vendor vendor, gsr_bitrate_mode bitrate_mode, AVDictionary **options) { - switch(bitrate_mode) { - case GSR_BITRATE_MODE_QP: { - if(video_codec_is_vulkan(video_codec)) - av_dict_set(options, "rc_mode", "cqp", 0); - else if(vendor == GSR_GPU_VENDOR_NVIDIA) - av_dict_set(options, "rc", "constqp", 0); - else - av_dict_set(options, "rc_mode", "CQP", 0); - break; - } - case GSR_BITRATE_MODE_VBR: { - if(video_codec_is_vulkan(video_codec)) - av_dict_set(options, "rc_mode", "vbr", 0); - else if(vendor == GSR_GPU_VENDOR_NVIDIA) - av_dict_set(options, "rc", "vbr", 0); - else - av_dict_set(options, "rc_mode", "VBR", 0); - break; - } - case GSR_BITRATE_MODE_CBR: { - if(video_codec_is_vulkan(video_codec)) - av_dict_set(options, "rc_mode", "cbr", 0); - else if(vendor == GSR_GPU_VENDOR_NVIDIA) - av_dict_set(options, "rc", "cbr", 0); - else - av_dict_set(options, "rc_mode", "CBR", 0); - break; - } - } -} - -static void video_hardware_set_qp(AVCodecContext *codec_context, gsr_video_quality video_quality, bool hdr, AVDictionary **options) { - // 8 bit / 10 bit = 80% - const float qp_multiply = hdr ? 8.0f/10.0f : 1.0f; - av_dict_set_int(options, "qp", video_quality_to_codec_quality_value(codec_context->codec_id, video_quality) * qp_multiply, 0); -} - -static void open_video_hardware(AVCodecContext *codec_context, bool low_power, const gsr_egl &egl, const args_parser &arg_parser) { - const gsr_color_depth color_depth = video_codec_to_bit_depth(arg_parser.video_codec); - const bool hdr = video_codec_is_hdr(arg_parser.video_codec); - AVDictionary *options = nullptr; - - if(arg_parser.bitrate_mode == GSR_BITRATE_MODE_QP) - video_hardware_set_qp(codec_context, arg_parser.video_quality, hdr, &options); - - video_set_rc(arg_parser.video_codec, egl.gpu_info.vendor, arg_parser.bitrate_mode, &options); - - // TODO: Enable multipass - - dict_set_profile(codec_context, egl.gpu_info.vendor, color_depth, arg_parser.video_codec, &options); - - if(video_codec_is_vulkan(arg_parser.video_codec)) { - av_dict_set_int(&options, "async_depth", 3, 0); - av_dict_set(&options, "tune", "ll", 0); // Low latency - av_dict_set(&options, "usage", arg_parser.is_livestream ? "stream" : "record", 0); - av_dict_set(&options, "content", "rendered", 0); // Game or 3D content - - if(codec_context->codec_id == AV_CODEC_ID_H264) { - // Removed because it causes stutter in games for some people - //av_dict_set_int(&options, "quality", 5, 0); // quality preset - } else if(codec_context->codec_id == AV_CODEC_ID_AV1) { - av_dict_set(&options, "tier", "main", 0); - } else if(codec_context->codec_id == AV_CODEC_ID_HEVC) { - if(hdr) - av_dict_set(&options, "sei", "hdr", 0); - } - } else if(egl.gpu_info.vendor == GSR_GPU_VENDOR_NVIDIA) { - // TODO: These dont seem to be necessary - // av_dict_set_int(&options, "zerolatency", 1, 0); - // if(codec_context->codec_id == AV_CODEC_ID_AV1) { - // av_dict_set(&options, "tune", "ll", 0); - // } else if(codec_context->codec_id == AV_CODEC_ID_H264 || codec_context->codec_id == AV_CODEC_ID_HEVC) { - // av_dict_set(&options, "preset", "llhq", 0); - // av_dict_set(&options, "tune", "ll", 0); - // } - av_dict_set(&options, "tune", "ll", 0); - av_dict_set_int(&options, "forced-idr", 1, 0); - - switch(arg_parser.tune) { - case GSR_TUNE_PERFORMANCE: - //av_dict_set(&options, "multipass", "qres", 0); - break; - case GSR_TUNE_QUALITY: - av_dict_set(&options, "multipass", "fullres", 0); - av_dict_set(&options, "preset", "p6", 0); - av_dict_set_int(&options, "rc-lookahead", 0, 0); - break; - } - - if(codec_context->codec_id == AV_CODEC_ID_H264) { - // TODO: h264 10bit? - // TODO: - // switch(pixel_format) { - // case GSR_PIXEL_FORMAT_YUV420: - // av_dict_set_int(&options, "profile", AV_PROFILE_H264_HIGH, 0); - // break; - // case GSR_PIXEL_FORMAT_YUV444: - // av_dict_set_int(&options, "profile", AV_PROFILE_H264_HIGH_444, 0); - // break; - // } - } else if(codec_context->codec_id == AV_CODEC_ID_AV1) { - switch(arg_parser.pixel_format) { - case GSR_PIXEL_FORMAT_YUV420: - av_dict_set(&options, "rgb_mode", "yuv420", 0); - break; - case GSR_PIXEL_FORMAT_YUV444: - av_dict_set(&options, "rgb_mode", "yuv444", 0); - break; - } - } else if(codec_context->codec_id == AV_CODEC_ID_HEVC) { - //av_dict_set(&options, "pix_fmt", "yuv420p16le", 0); - } - } else { - // TODO: More quality options - if(low_power) - av_dict_set_int(&options, "low_power", 1, 0); - // Improves performance but increases vram. - // TODO: Might need a different async_depth for optimal performance on different amd/intel gpus - av_dict_set_int(&options, "async_depth", 3, 0); - - if(codec_context->codec_id == AV_CODEC_ID_H264) { - // Removed because it causes stutter in games for some people - //av_dict_set_int(&options, "quality", 5, 0); // quality preset - } else if(codec_context->codec_id == AV_CODEC_ID_AV1) { - av_dict_set(&options, "tier", "main", 0); - } else if(codec_context->codec_id == AV_CODEC_ID_HEVC) { - if(hdr) - av_dict_set(&options, "sei", "hdr", 0); - } - - // TODO: vp8/vp9 10bit - } - - if(codec_context->codec_id == AV_CODEC_ID_H264) { - av_dict_set(&options, "coder", "cabac", 0); // TODO: cavlc is faster than cabac but worse compression. Which to use? - } - - av_dict_set(&options, "strict", "experimental", 0); - - if(arg_parser.ffmpeg_video_opts) - av_dict_parse_string(&options, arg_parser.ffmpeg_video_opts, "=", ";", 0); - - int ret = avcodec_open2(codec_context, codec_context->codec, &options); - if (ret < 0) { - fprintf(stderr, "gsr error: Could not open video codec: %s\n", av_error_to_string(ret)); - _exit(1); - } -} - -static const int save_replay_seconds_full = -1; - -static sig_atomic_t running = 1; -static sig_atomic_t toggle_pause = 0; -static sig_atomic_t toggle_replay_recording = 0; -static sig_atomic_t save_replay_seconds = 0; - -static void stop_handler(int) { - running = 0; -} - -static void toggle_pause_handler(int) { - toggle_pause = 1; -} - -static void toggle_replay_recording_handler(int) { - toggle_replay_recording = 1; -} - -static void save_replay_handler(int) { - save_replay_seconds = save_replay_seconds_full; -} - -static void save_replay_10_seconds_handler(int) { - save_replay_seconds = 10; -} - -static void save_replay_30_seconds_handler(int) { - save_replay_seconds = 30; -} - -static void save_replay_1_minute_handler(int) { - save_replay_seconds = 60; -} - -static void save_replay_5_minutes_handler(int) { - save_replay_seconds = 60*5; -} - -static void save_replay_10_minutes_handler(int) { - save_replay_seconds = 60*10; -} - -static void save_replay_30_minutes_handler(int) { - save_replay_seconds = 60*30; -} - -static std::string get_date_str() { - char str[128]; - time_t now = time(NULL); - struct tm *t = localtime(&now); - strftime(str, sizeof(str)-1, "%Y-%m-%d_%H-%M-%S", t); - return str; -} - -static std::string get_date_only_str() { - char str[128]; - time_t now = time(NULL); - struct tm *t = localtime(&now); - strftime(str, sizeof(str)-1, "%Y-%m-%d", t); - return str; -} - -static std::string get_time_only_str() { - char str[128]; - time_t now = time(NULL); - struct tm *t = localtime(&now); - strftime(str, sizeof(str)-1, "%H-%M-%S", t); - return str; -} - -static AVStream* create_stream(AVFormatContext *av_format_context, AVCodecContext *codec_context) { - AVStream *stream = avformat_new_stream(av_format_context, nullptr); - if (!stream) { - fprintf(stderr, "gsr error: Could not allocate stream\n"); - _exit(1); - } - stream->id = av_format_context->nb_streams - 1; - stream->time_base = codec_context->time_base; - stream->avg_frame_rate = codec_context->framerate; - //stream->r_frame_rate = codec_context->framerate; - return stream; -} - -static void run_recording_saved_script_async(const char *script_file, const char *video_file, const char *type) { - char script_file_full[PATH_MAX]; - script_file_full[0] = '\0'; - if(!realpath(script_file, script_file_full)) { - fprintf(stderr, "gsr error: script file not found: %s\n", script_file); - return; - } - - const char *args[7]; - const bool inside_flatpak = getenv("FLATPAK_ID") != NULL; - - if(inside_flatpak) { - args[0] = "flatpak-spawn"; - args[1] = "--host"; - args[2] = "--"; - args[3] = script_file_full; - args[4] = video_file; - args[5] = type; - args[6] = NULL; - } else { - args[0] = script_file_full; - args[1] = video_file; - args[2] = type; - args[3] = NULL; - } - - pid_t pid = fork(); - if(pid == -1) { - perror(script_file_full); - return; - } else if(pid == 0) { // child - setsid(); - signal(SIGHUP, SIG_IGN); - - pid_t second_child = fork(); - if(second_child == 0) { // child - execvp(args[0], (char* const*)args); - perror(script_file_full); - _exit(127); - } else if(second_child != -1) { // parent - _exit(0); - } - } else { // parent - waitpid(pid, NULL, 0); - } -} - -static double audio_codec_get_desired_delay(gsr_audio_codec audio_codec, int fps) { - const double fps_inv = 1.0 / (double)fps; - const double base = 0.01 + 1.0/165.0; - switch(audio_codec) { - case GSR_AUDIO_CODEC_OPUS: - return std::max(0.0, base - fps_inv); - case GSR_AUDIO_CODEC_AAC: - return std::max(0.0, (base + 0.008) * 2.0 - fps_inv); - case GSR_AUDIO_CODEC_FLAC: - // TODO: Test - return std::max(0.0, base - fps_inv); - } - assert(false); - return std::max(0.0, base - fps_inv); -} - -struct AudioDeviceData { - SoundDevice sound_device; - AudioInput audio_input; - AVFilterContext *src_filter_ctx = nullptr; - AVFrame *frame = nullptr; - std::thread thread; // TODO: Instead of having a thread for each track, have one thread for all threads and read the data with non-blocking read -}; - -// TODO: Cleanup -struct AudioTrack { - std::string name; - AVCodecContext *codec_context = nullptr; - - std::vector<AudioDeviceData> audio_devices; - AVFilterGraph *graph = nullptr; - AVFilterContext *sink = nullptr; - int stream_index = 0; - int64_t pts = 0; -}; - -static bool add_hdr_metadata_to_video_stream(gsr_capture *cap, AVStream *video_stream) { - size_t light_metadata_size = 0; - size_t mastering_display_metadata_size = 0; - AVContentLightMetadata *light_metadata = av_content_light_metadata_alloc(&light_metadata_size); - #if LIBAVUTIL_VERSION_INT < AV_VERSION_INT(59, 37, 100) - AVMasteringDisplayMetadata *mastering_display_metadata = av_mastering_display_metadata_alloc(); - mastering_display_metadata_size = sizeof(*mastering_display_metadata); - #else - AVMasteringDisplayMetadata *mastering_display_metadata = av_mastering_display_metadata_alloc_size(&mastering_display_metadata_size); - #endif - - if(!light_metadata || !mastering_display_metadata) { - if(light_metadata) - av_freep(&light_metadata); - - if(mastering_display_metadata) - av_freep(&mastering_display_metadata); - - return false; - } - - if(!gsr_capture_set_hdr_metadata(cap, mastering_display_metadata, light_metadata)) { - av_freep(&light_metadata); - av_freep(&mastering_display_metadata); - return false; - } - - // TODO: More error checking - - #if LIBAVCODEC_VERSION_INT < AV_VERSION_INT(60, 31, 102) - const bool content_light_level_added = av_stream_add_side_data(video_stream, AV_PKT_DATA_CONTENT_LIGHT_LEVEL, (uint8_t*)light_metadata, light_metadata_size) == 0; - #else - const bool content_light_level_added = av_packet_side_data_add(&video_stream->codecpar->coded_side_data, &video_stream->codecpar->nb_coded_side_data, AV_PKT_DATA_CONTENT_LIGHT_LEVEL, light_metadata, light_metadata_size, 0) != NULL; - #endif - - #if LIBAVCODEC_VERSION_INT < AV_VERSION_INT(60, 31, 102) - const bool mastering_display_metadata_added = av_stream_add_side_data(video_stream, AV_PKT_DATA_MASTERING_DISPLAY_METADATA, (uint8_t*)mastering_display_metadata, mastering_display_metadata_size) == 0; - #else - const bool mastering_display_metadata_added = av_packet_side_data_add(&video_stream->codecpar->coded_side_data, &video_stream->codecpar->nb_coded_side_data, AV_PKT_DATA_MASTERING_DISPLAY_METADATA, mastering_display_metadata, mastering_display_metadata_size, 0) != NULL; - #endif - - if(!content_light_level_added) - av_freep(&light_metadata); - - if(!mastering_display_metadata_added) - av_freep(&mastering_display_metadata); - - // Return true even on failure because we dont want to retry adding hdr metadata on failure - return true; -} - -static void set_format_context_options(AVFormatContext *av_format_context) { - if(LIBAVFORMAT_VERSION_INT >= AV_VERSION_INT(62, 6, 101)) { - av_opt_set(av_format_context->priv_data, "use_editlist", "1", 0); - const AVOption *opt = av_opt_find(av_format_context->priv_data, "movflags", NULL, 0, 0); - if (opt && opt->unit) { - const AVOption *flag = av_opt_find(av_format_context->priv_data, "hybrid_fragmented", opt->unit, 0, 0); - if (flag) - av_opt_set(av_format_context->priv_data, "movflags", "+hybrid_fragmented", 0); - } - } else { - const AVOutputFormat *output_format = av_format_context->oformat; - const char *file_extension = output_format->extensions ? output_format->extensions : ""; - if(strcmp(file_extension, "mp4") != 0 && strcmp(file_extension, "mov") != 0) - return; - - fprintf(stderr, "gsr warning: your FFmpeg version is known to be buggy (it doesn't have working hybrid_fragmented movflags). If you experience stutter in the mp4 file then update your FFmpeg to at least version 8 or record to a mkv file\n"); - } -} - -struct RecordingStartAudio { - const AudioTrack *audio_track; - AVStream *stream; -}; - -struct RecordingStartResult { - AVFormatContext *av_format_context = nullptr; - AVStream *video_stream = nullptr; - std::vector<RecordingStartAudio> audio_inputs; -}; - -typedef enum { - VVEC2I_TYPE_PIXELS, - VVEC2I_TYPE_SCALAR -} vvec2i_type; - -typedef struct { - int x, y; - vvec2i_type x_type; - vvec2i_type y_type; -} vvec2i; - -struct CaptureSource { - std::string name; - CaptureSourceType type = GSR_CAPTURE_SOURCE_TYPE_WINDOW; - gsr_capture_alignment halign = GSR_CAPTURE_ALIGN_CENTER; - gsr_capture_alignment valign = GSR_CAPTURE_ALIGN_CENTER; - gsr_capture_v4l2_pixfmt v4l2_pixfmt = GSR_CAPTURE_V4L2_PIXFMT_AUTO; - uint32_t flip = GSR_FLIP_NONE; - vvec2i pos = {0, 0, VVEC2I_TYPE_PIXELS, VVEC2I_TYPE_PIXELS}; - vvec2i size = {100, 100, VVEC2I_TYPE_SCALAR, VVEC2I_TYPE_SCALAR}; - vec2i region_pos = {0, 0}; - vec2i region_size = {0, 0}; - bool region_set = false; - int64_t window_id = 0; - int camera_fps = 0; - vec2i camera_resolution = {0, 0}; -}; - -struct VideoSource { - gsr_capture *capture; - gsr_capture_metadata metadata; - CaptureSource *capture_source; -}; - -static RecordingStartResult start_recording_create_streams(const char *filename, const args_parser &arg_parser, AVCodecContext *video_codec_context, const std::vector<AudioTrack> &audio_tracks, bool hdr, std::vector<VideoSource> &video_sources) { - AVFormatContext *av_format_context; - avformat_alloc_output_context2(&av_format_context, nullptr, arg_parser.container_format, filename); - set_format_context_options(av_format_context); - - AVStream *video_stream = create_stream(av_format_context, video_codec_context); - avcodec_parameters_from_context(video_stream->codecpar, video_codec_context); - - RecordingStartResult result; - result.audio_inputs.reserve(audio_tracks.size()); - - for(const AudioTrack &audio_track : audio_tracks) { - AVStream *audio_stream = create_stream(av_format_context, audio_track.codec_context); - if(!audio_track.name.empty() && !arg_parser.exclude_metadata) - av_dict_set(&audio_stream->metadata, "title", audio_track.name.c_str(), 0); - avcodec_parameters_from_context(audio_stream->codecpar, audio_track.codec_context); - result.audio_inputs.push_back({&audio_track, audio_stream}); - } - - const int open_ret = avio_open(&av_format_context->pb, filename, AVIO_FLAG_WRITE); - if(open_ret < 0) { - fprintf(stderr, "gsr error: start_recording_create_streams: could not open '%s': %s\n", filename, av_error_to_string(open_ret)); - return result; - } - - AVDictionary *options = nullptr; - av_dict_set(&options, "strict", "experimental", 0); - - if(arg_parser.ffmpeg_opts) - av_dict_parse_string(&options, arg_parser.ffmpeg_opts, "=", ";", 0); - - const int header_write_ret = avformat_write_header(av_format_context, &options); - av_dict_free(&options); - if(header_write_ret < 0) { - fprintf(stderr, "gsr error: start_recording_create_streams: error occurred when writing header to output file: %s\n", av_error_to_string(header_write_ret)); - avio_close(av_format_context->pb); - avformat_free_context(av_format_context); - return result; - } - - for(VideoSource &video_source : video_sources) { - if(hdr && add_hdr_metadata_to_video_stream(video_source.capture, video_stream)) - break; - } - - result.av_format_context = av_format_context; - result.video_stream = video_stream; - return result; -} - -static bool stop_recording_close_streams(AVFormatContext *av_format_context) { - bool trailer_written = true; - if(av_write_trailer(av_format_context) != 0) { - //fprintf(stderr, "gsr error: end: failed to write trailer\n"); - //trailer_written = false; - } - - const bool closed = avio_close(av_format_context->pb) == 0; - avformat_free_context(av_format_context); - return trailer_written && closed; -} - -static std::future<bool> save_replay_thread; -static std::string save_replay_output_filepath; - -static std::string create_new_recording_filepath_from_timestamp(std::string directory, const char *filename_prefix, const std::string &file_extension, bool date_folders) { - std::string output_filepath; - if(date_folders) { - std::string output_folder = directory + '/' + get_date_only_str(); - if(create_directory_recursive(&output_folder[0]) != 0) - fprintf(stderr, "gsr error: failed to create directory: %s\n", output_folder.c_str()); - output_filepath = output_folder + "/" + filename_prefix + "_" + get_time_only_str() + "." + file_extension; - } else { - if(create_directory_recursive(&directory[0]) != 0) - fprintf(stderr, "gsr error: failed to create directory: %s\n", directory.c_str()); - output_filepath = directory + "/" + filename_prefix + "_" + get_date_str() + "." + file_extension; - } - return output_filepath; -} - -static RecordingStartAudio* get_recording_start_item_by_stream_index(RecordingStartResult &result, int stream_index) { - for(auto &audio_input : result.audio_inputs) { - if(audio_input.stream->index == stream_index) - return &audio_input; - } - return nullptr; -} - -struct AudioPtsOffset { - int64_t pts_offset = 0; - int stream_index = 0; -}; - -static bool save_replay_async(AVCodecContext *video_codec_context, int video_stream_index, const std::vector<AudioTrack> &audio_tracks, gsr_encoder *encoder, const args_parser &arg_parser, const std::string &file_extension, bool date_folders, bool hdr, std::vector<VideoSource> &video_sources, int current_save_replay_seconds) { - if(save_replay_thread.valid()) - return true; - - pthread_mutex_lock(&encoder->replay_mutex); - gsr_replay_buffer *cloned_replay_buffer = gsr_replay_buffer_clone(encoder->replay_buffer); - pthread_mutex_unlock(&encoder->replay_mutex); - if(!cloned_replay_buffer) { - // TODO: Return this error to mark the replay as failed - fprintf(stderr, "gsr error: failed to save replay: failed to clone replay buffer\n"); - return false; - } - - const gsr_replay_buffer_iterator search_start_iterator = current_save_replay_seconds == save_replay_seconds_full ? gsr_replay_buffer_iterator{0, 0} : gsr_replay_buffer_find_packet_index_by_time_passed(cloned_replay_buffer, current_save_replay_seconds); - const gsr_replay_buffer_iterator video_start_iterator = gsr_replay_buffer_find_keyframe(cloned_replay_buffer, search_start_iterator, video_stream_index, false); - if(video_start_iterator.packet_index == (size_t)-1) { - fprintf(stderr, "gsr error: failed to save replay: failed to find a video keyframe. perhaps replay was saved too fast, before anything has been recorded\n"); - pthread_mutex_lock(&encoder->replay_mutex); - gsr_replay_buffer_destroy(cloned_replay_buffer); - pthread_mutex_unlock(&encoder->replay_mutex); - return true; - } - - const int64_t video_pts_offset = gsr_replay_buffer_iterator_get_packet(cloned_replay_buffer, video_start_iterator)->pts; - - std::vector<AudioPtsOffset> audio_pts_offsets; - audio_pts_offsets.reserve(audio_tracks.size()); - for(const AudioTrack &audio_track : audio_tracks) { - const gsr_replay_buffer_iterator audio_start_iterator = gsr_replay_buffer_find_keyframe(cloned_replay_buffer, video_start_iterator, audio_track.stream_index, false); - const int64_t audio_pts_offset = audio_start_iterator.packet_index == (size_t)-1 ? 0 : gsr_replay_buffer_iterator_get_packet(cloned_replay_buffer, audio_start_iterator)->pts; - audio_pts_offsets.push_back(AudioPtsOffset{audio_pts_offset, audio_track.stream_index}); - } - - std::string output_filepath = create_new_recording_filepath_from_timestamp(arg_parser.filename, "Replay", file_extension, date_folders); - RecordingStartResult recording_start_result = start_recording_create_streams(output_filepath.c_str(), arg_parser, video_codec_context, audio_tracks, hdr, video_sources); - if(!recording_start_result.av_format_context) { - pthread_mutex_lock(&encoder->replay_mutex); - gsr_replay_buffer_destroy(cloned_replay_buffer); - pthread_mutex_unlock(&encoder->replay_mutex); - return false; - } - - save_replay_output_filepath = std::move(output_filepath); - - save_replay_thread = std::async(std::launch::async, [video_stream_index, recording_start_result, video_start_iterator, video_pts_offset, audio_pts_offsets{std::move(audio_pts_offsets)}, video_codec_context, cloned_replay_buffer, encoder]() mutable { - bool success = true; - gsr_replay_buffer_iterator replay_iterator = video_start_iterator; - - for(;;) { - AVPacket *replay_packet = gsr_replay_buffer_iterator_get_packet(cloned_replay_buffer, replay_iterator); - uint8_t *replay_packet_data = NULL; - if(replay_packet) { - pthread_mutex_lock(&encoder->replay_mutex); - replay_packet_data = gsr_replay_buffer_iterator_get_packet_data(cloned_replay_buffer, replay_iterator); - pthread_mutex_unlock(&encoder->replay_mutex); - } - - if(!replay_packet) { - fprintf(stderr, "gsr error: save_replay_async: no replay packet\n"); - success = false; - break; - } - - if(!replay_packet->data && !replay_packet_data) { - fprintf(stderr, "gsr error: save_replay_async: no replay packet data\n"); - success = false; - break; - } - - // TODO: Check if successful - AVPacket av_packet; - memset(&av_packet, 0, sizeof(av_packet)); - //av_packet_from_data(av_packet, replay_packet->data, replay_packet->size); - av_packet.data = replay_packet->data ? replay_packet->data : replay_packet_data; - av_packet.size = replay_packet->size; - av_packet.stream_index = replay_packet->stream_index; - av_packet.pts = replay_packet->pts; - av_packet.dts = replay_packet->pts; - av_packet.flags = replay_packet->flags; - //av_packet.duration = replay_packet->duration; - - AVStream *stream = recording_start_result.video_stream; - AVCodecContext *codec_context = video_codec_context; - - if(av_packet.stream_index == video_stream_index) { - av_packet.pts -= video_pts_offset; - av_packet.dts -= video_pts_offset; - } else { - RecordingStartAudio *recording_start_audio = get_recording_start_item_by_stream_index(recording_start_result, av_packet.stream_index); - if(!recording_start_audio) { - fprintf(stderr, "gsr error: save_replay_async: failed to find audio stream by index: %d\n", av_packet.stream_index); - free(replay_packet_data); - continue; - } - - const AudioTrack *audio_track = recording_start_audio->audio_track; - stream = recording_start_audio->stream; - codec_context = audio_track->codec_context; - - const AudioPtsOffset &audio_pts_offset = audio_pts_offsets[av_packet.stream_index - 1]; - assert(audio_pts_offset.stream_index == av_packet.stream_index); - av_packet.pts -= audio_pts_offset.pts_offset; - av_packet.dts -= audio_pts_offset.pts_offset; - } - - //av_packet.stream_index = stream->index; - av_packet_rescale_ts(&av_packet, codec_context->time_base, stream->time_base); - - const int ret = av_write_frame(recording_start_result.av_format_context, &av_packet); - if(ret < 0) - fprintf(stderr, "gsr error: Failed to write frame index %d to muxer, reason: %s (%d)\n", av_packet.stream_index, av_error_to_string(ret), ret); - - free(replay_packet_data); - - //av_packet_free(&av_packet); - if(!gsr_replay_buffer_iterator_next(cloned_replay_buffer, &replay_iterator)) - break; - } - - stop_recording_close_streams(recording_start_result.av_format_context); - - pthread_mutex_lock(&encoder->replay_mutex); - gsr_replay_buffer_destroy(cloned_replay_buffer); - pthread_mutex_unlock(&encoder->replay_mutex); - - return success; - }); - - return true; -} - -static void split_string(const std::string &str, char delimiter, std::function<bool(const char*,size_t)> callback) { - size_t index = 0; - while(index < str.size()) { - size_t end_index = str.find(delimiter, index); - if(end_index == std::string::npos) - end_index = str.size(); - - if(!callback(&str[index], end_index - index)) - break; - - index = end_index + 1; - } -} - -static bool string_starts_with(const char *str, size_t str_size, const char *substr) { - int len = strlen(substr); - return (int)str_size >= len && memcmp(str, substr, len) == 0; -} - -static bool string_starts_with(const std::string &str, const char *substr) { - return string_starts_with(str.data(), str.size(), substr); -} - -static bool string_ends_with(const char *str, const char *substr) { - int str_len = strlen(str); - int substr_len = strlen(substr); - return str_len >= substr_len && memcmp(str + str_len - substr_len, substr, substr_len) == 0; -} - -static const AudioDevice* get_audio_device_by_name(const std::vector<AudioDevice> &audio_devices, const char *name) { - for(const auto &audio_device : audio_devices) { - if(strcmp(audio_device.name.c_str(), name) == 0) - return &audio_device; - } - return nullptr; -} - -static MergedAudioInputs parse_audio_input_arg(const char *str) { - MergedAudioInputs result; - result.track_name = str; - - split_string(str, '|', [&](const char *sub, size_t size) { - if(size == 0) - return true; - - AudioInput audio_input; - audio_input.name.assign(sub, size); - - if(string_starts_with(audio_input.name.c_str(), "name:")) { - result.custom_name = audio_input.name.substr(5); - return true; - } else if(string_starts_with(audio_input.name.c_str(), "app:")) { - audio_input.name.erase(audio_input.name.begin(), audio_input.name.begin() + 4); - audio_input.type = AudioInputType::APPLICATION; - audio_input.inverted = false; - result.audio_inputs.push_back(std::move(audio_input)); - return true; - } else if(string_starts_with(audio_input.name.c_str(), "app-inverse:")) { - audio_input.name.erase(audio_input.name.begin(), audio_input.name.begin() + 12); - audio_input.type = AudioInputType::APPLICATION; - audio_input.inverted = true; - result.audio_inputs.push_back(std::move(audio_input)); - return true; - } else if(string_starts_with(audio_input.name.c_str(), "device:")) { - audio_input.name.erase(audio_input.name.begin(), audio_input.name.begin() + 7); - audio_input.type = AudioInputType::DEVICE; - result.audio_inputs.push_back(std::move(audio_input)); - return true; - } else { - audio_input.type = AudioInputType::DEVICE; - result.audio_inputs.push_back(std::move(audio_input)); - return true; - } - }); - - return result; -} - -static int init_filter_graph(AVCodecContext* audio_codec_context, AVFilterGraph** graph, AVFilterContext** sink, std::vector<AVFilterContext*>& src_filter_ctx, size_t num_sources) { - char ch_layout[64]; - int err = 0; - ch_layout[0] = '\0'; - - // C89-style variable declaration to - // avoid problems because of goto - AVFilterGraph* filter_graph = nullptr; - AVFilterContext* mix_ctx = nullptr; - - const AVFilter* mix_filter = nullptr; - const AVFilter* abuffersink = nullptr; - AVFilterContext* abuffersink_ctx = nullptr; - char args[512] = { 0 }; -#if LIBAVFILTER_VERSION_INT >= AV_VERSION_INT(7, 107, 100) - bool normalize = false; -#endif - - filter_graph = avfilter_graph_alloc(); - if (!filter_graph) { - fprintf(stderr, "Unable to create filter graph.\n"); - err = AVERROR(ENOMEM); - goto fail; - } - - for(size_t i = 0; i < num_sources; ++i) { - const AVFilter *abuffer = avfilter_get_by_name("abuffer"); - if (!abuffer) { - fprintf(stderr, "Could not find the abuffer filter.\n"); - err = AVERROR_FILTER_NOT_FOUND; - goto fail; - } - - AVFilterContext *abuffer_ctx = avfilter_graph_alloc_filter(filter_graph, abuffer, NULL); - if (!abuffer_ctx) { - fprintf(stderr, "Could not allocate the abuffer instance.\n"); - err = AVERROR(ENOMEM); - goto fail; - } - - #if LIBAVCODEC_VERSION_MAJOR < 60 - av_get_channel_layout_string(ch_layout, sizeof(ch_layout), 0, AV_CH_LAYOUT_STEREO); - #else - av_channel_layout_describe(&audio_codec_context->ch_layout, ch_layout, sizeof(ch_layout)); - #endif - av_opt_set (abuffer_ctx, "channel_layout", ch_layout, AV_OPT_SEARCH_CHILDREN); - av_opt_set (abuffer_ctx, "sample_fmt", av_get_sample_fmt_name(audio_codec_context->sample_fmt), AV_OPT_SEARCH_CHILDREN); - av_opt_set_q (abuffer_ctx, "time_base", audio_codec_context->time_base, AV_OPT_SEARCH_CHILDREN); - av_opt_set_int(abuffer_ctx, "sample_rate", audio_codec_context->sample_rate, AV_OPT_SEARCH_CHILDREN); - av_opt_set_int(abuffer_ctx, "bit_rate", audio_codec_context->bit_rate, AV_OPT_SEARCH_CHILDREN); - - err = avfilter_init_str(abuffer_ctx, NULL); - if (err < 0) { - fprintf(stderr, "Could not initialize the abuffer filter.\n"); - goto fail; - } - - src_filter_ctx.push_back(abuffer_ctx); - } - - mix_filter = avfilter_get_by_name("amix"); - if (!mix_filter) { - av_log(NULL, AV_LOG_ERROR, "Could not find the mix filter.\n"); - err = AVERROR_FILTER_NOT_FOUND; - goto fail; - } - -#if LIBAVFILTER_VERSION_INT >= AV_VERSION_INT(7, 107, 100) - snprintf(args, sizeof(args), "inputs=%d:normalize=%s", (int)num_sources, normalize ? "true" : "false"); -#else - snprintf(args, sizeof(args), "inputs=%d", (int)num_sources); - fprintf(stderr, "gsr warning: your ffmpeg version doesn't support disabling normalizing of mixed audio. Volume might be lower than expected\n"); -#endif - - err = avfilter_graph_create_filter(&mix_ctx, mix_filter, "amix", args, NULL, filter_graph); - if (err < 0) { - av_log(NULL, AV_LOG_ERROR, "Cannot create audio amix filter\n"); - goto fail; - } - - abuffersink = avfilter_get_by_name("abuffersink"); - if (!abuffersink) { - fprintf(stderr, "Could not find the abuffersink filter.\n"); - err = AVERROR_FILTER_NOT_FOUND; - goto fail; - } - - abuffersink_ctx = avfilter_graph_alloc_filter(filter_graph, abuffersink, "sink"); - if (!abuffersink_ctx) { - fprintf(stderr, "Could not allocate the abuffersink instance.\n"); - err = AVERROR(ENOMEM); - goto fail; - } - - err = avfilter_init_str(abuffersink_ctx, NULL); - if (err < 0) { - fprintf(stderr, "Could not initialize the abuffersink instance.\n"); - goto fail; - } - - err = 0; - for(size_t i = 0; i < src_filter_ctx.size(); ++i) { - AVFilterContext *src_ctx = src_filter_ctx[i]; - if (err >= 0) - err = avfilter_link(src_ctx, 0, mix_ctx, i); - } - if (err >= 0) - err = avfilter_link(mix_ctx, 0, abuffersink_ctx, 0); - if (err < 0) { - av_log(NULL, AV_LOG_ERROR, "Error connecting filters\n"); - goto fail; - } - - err = avfilter_graph_config(filter_graph, NULL); - if (err < 0) { - av_log(NULL, AV_LOG_ERROR, "Error configuring the filter graph\n"); - goto fail; - } - - /* Make sure the sink always outputs frames with the exact amount of samples the audio encoder wants, - otherwise the audio encoder rejects the frame and that piece of audio is lost */ - av_buffersink_set_frame_size(abuffersink_ctx, audio_codec_context->frame_size); - - *graph = filter_graph; - *sink = abuffersink_ctx; - - return 0; - -fail: - avfilter_graph_free(&filter_graph); - src_filter_ctx.clear(); // possibly unnecessary? - return err; -} - -static gsr_video_encoder* create_video_encoder(gsr_egl *egl, const args_parser &arg_parser) { - const gsr_color_depth color_depth = video_codec_to_bit_depth(arg_parser.video_codec); - gsr_video_encoder *video_encoder = nullptr; - - if(arg_parser.video_encoder == GSR_VIDEO_ENCODER_HW_CPU) { - gsr_video_encoder_software_params params; - params.egl = egl; - params.color_depth = color_depth; - video_encoder = gsr_video_encoder_software_create(¶ms); - return video_encoder; - } - - if(video_codec_is_vulkan(arg_parser.video_codec)) { - gsr_video_encoder_vulkan_params params; - params.egl = egl; - params.color_depth = color_depth; - video_encoder = gsr_video_encoder_vulkan_create(¶ms); - return video_encoder; - } - - switch(egl->gpu_info.vendor) { - case GSR_GPU_VENDOR_AMD: - case GSR_GPU_VENDOR_INTEL: - case GSR_GPU_VENDOR_BROADCOM: { - gsr_video_encoder_vaapi_params params; - params.egl = egl; - params.color_depth = color_depth; - video_encoder = gsr_video_encoder_vaapi_create(¶ms); - break; - } - case GSR_GPU_VENDOR_NVIDIA: { - gsr_video_encoder_nvenc_params params; - params.egl = egl; - params.color_depth = color_depth; - video_encoder = gsr_video_encoder_nvenc_create(¶ms); - break; - } - } - - return video_encoder; -} - -static bool get_supported_video_codecs(gsr_egl *egl, gsr_video_codec video_codec, bool use_software_video_encoder, bool cleanup, gsr_supported_video_codecs *video_codecs) { - memset(video_codecs, 0, sizeof(*video_codecs)); - - if(use_software_video_encoder) { - video_codecs->h264.supported = avcodec_find_encoder_by_name("libx264"); - video_codecs->h264.max_resolution = {4096, 2304}; - return true; - } - - if(video_codec_is_vulkan(video_codec)) - return gsr_get_supported_video_codecs_vulkan(video_codecs, egl->card_path, &egl->vulkan_device_index, cleanup); - - switch(egl->gpu_info.vendor) { - case GSR_GPU_VENDOR_AMD: - case GSR_GPU_VENDOR_INTEL: - case GSR_GPU_VENDOR_BROADCOM: - return gsr_get_supported_video_codecs_vaapi(video_codecs, egl->card_path, cleanup); - case GSR_GPU_VENDOR_NVIDIA: - return gsr_get_supported_video_codecs_nvenc(video_codecs, cleanup); - } - - return false; -} - -static void xwayland_check_callback(const gsr_monitor *monitor, void *userdata) { - bool *xwayland_found = (bool*)userdata; - if(monitor->name_len >= 8 && strncmp(monitor->name, "XWAYLAND", 8) == 0) - *xwayland_found = true; - else if(memmem(monitor->name, monitor->name_len, "X11", 3)) - *xwayland_found = true; -} - -static bool is_xwayland(Display *display) { - int opcode, event, error; - if(XQueryExtension(display, "XWAYLAND", &opcode, &event, &error)) - return true; - - bool xwayland_found = false; - for_each_active_monitor_output_x11_not_cached(display, xwayland_check_callback, &xwayland_found); - return xwayland_found; -} - -static bool is_using_prime_run() { - const char *prime_render_offload = getenv("__NV_PRIME_RENDER_OFFLOAD"); - return (prime_render_offload && strcmp(prime_render_offload, "1") == 0) || getenv("DRI_PRIME"); -} - -static void disable_prime_run() { - unsetenv("__NV_PRIME_RENDER_OFFLOAD"); - unsetenv("__NV_PRIME_RENDER_OFFLOAD_PROVIDER"); - unsetenv("__GLX_VENDOR_LIBRARY_NAME"); - unsetenv("__VK_LAYER_NV_optimus"); - unsetenv("DRI_PRIME"); -} - -static gsr_window* gsr_window_create(Display *display, bool wayland) { - if(wayland) - return gsr_window_wayland_create(); - else - return gsr_window_x11_create(display); -} - -static void list_system_info(bool wayland) { - printf("display_server|%s\n", wayland ? "wayland" : "x11"); - bool supports_app_audio = false; -#ifdef GSR_APP_AUDIO - supports_app_audio = pulseaudio_server_is_pipewire(); - if(supports_app_audio) { - gsr_pipewire_audio audio; - if(gsr_pipewire_audio_init(&audio)) - gsr_pipewire_audio_deinit(&audio); - else - supports_app_audio = false; - } -#endif - printf("supports_app_audio|%s\n", supports_app_audio ? "yes" : "no"); -} - -static void list_gpu_info(gsr_egl *egl) { - switch(egl->gpu_info.vendor) { - case GSR_GPU_VENDOR_AMD: - printf("vendor|amd\n"); - break; - case GSR_GPU_VENDOR_INTEL: - printf("vendor|intel\n"); - break; - case GSR_GPU_VENDOR_NVIDIA: - printf("vendor|nvidia\n"); - break; - case GSR_GPU_VENDOR_BROADCOM: - printf("vendor|broadcom\n"); - break; - } - printf("card_path|%s\n", egl->card_path); -} - -static const AVCodec* get_ffmpeg_video_codec(gsr_video_codec video_codec, gsr_gpu_vendor vendor) { - switch(video_codec) { - case GSR_VIDEO_CODEC_H264: - return avcodec_find_encoder_by_name(vendor == GSR_GPU_VENDOR_NVIDIA ? "h264_nvenc" : "h264_vaapi"); - case GSR_VIDEO_CODEC_HEVC: - case GSR_VIDEO_CODEC_HEVC_HDR: - case GSR_VIDEO_CODEC_HEVC_10BIT: - return avcodec_find_encoder_by_name(vendor == GSR_GPU_VENDOR_NVIDIA ? "hevc_nvenc" : "hevc_vaapi"); - case GSR_VIDEO_CODEC_AV1: - case GSR_VIDEO_CODEC_AV1_HDR: - case GSR_VIDEO_CODEC_AV1_10BIT: - return avcodec_find_encoder_by_name(vendor == GSR_GPU_VENDOR_NVIDIA ? "av1_nvenc" : "av1_vaapi"); - case GSR_VIDEO_CODEC_VP8: - return avcodec_find_encoder_by_name(vendor == GSR_GPU_VENDOR_NVIDIA ? "vp8_nvenc" : "vp8_vaapi"); - case GSR_VIDEO_CODEC_VP9: - return avcodec_find_encoder_by_name(vendor == GSR_GPU_VENDOR_NVIDIA ? "vp9_nvenc" : "vp9_vaapi"); - case GSR_VIDEO_CODEC_H264_VULKAN: - return avcodec_find_encoder_by_name("h264_vulkan"); - case GSR_VIDEO_CODEC_HEVC_VULKAN: - case GSR_VIDEO_CODEC_HEVC_HDR_VULKAN: - case GSR_VIDEO_CODEC_HEVC_10BIT_VULKAN: - return avcodec_find_encoder_by_name("hevc_vulkan"); - case GSR_VIDEO_CODEC_AV1_VULKAN: - case GSR_VIDEO_CODEC_AV1_HDR_VULKAN: - case GSR_VIDEO_CODEC_AV1_10BIT_VULKAN: - return avcodec_find_encoder_by_name("av1_vulkan"); - } - return nullptr; -} - -static void set_supported_video_codecs_ffmpeg(gsr_supported_video_codecs *supported_video_codecs, gsr_supported_video_codecs *supported_video_codecs_vulkan, gsr_gpu_vendor vendor) { - if(!get_ffmpeg_video_codec(GSR_VIDEO_CODEC_H264, vendor)) { - supported_video_codecs->h264.supported = false; - } - - if(!get_ffmpeg_video_codec(GSR_VIDEO_CODEC_HEVC, vendor)) { - supported_video_codecs->hevc.supported = false; - supported_video_codecs->hevc_hdr.supported = false; - supported_video_codecs->hevc_10bit.supported = false; - } - - if(!get_ffmpeg_video_codec(GSR_VIDEO_CODEC_AV1, vendor)) { - supported_video_codecs->av1.supported = false; - supported_video_codecs->av1_hdr.supported = false; - supported_video_codecs->av1_10bit.supported = false; - } - - if(!get_ffmpeg_video_codec(GSR_VIDEO_CODEC_VP8, vendor)) { - supported_video_codecs->vp8.supported = false; - } - - if(!get_ffmpeg_video_codec(GSR_VIDEO_CODEC_VP9, vendor)) { - supported_video_codecs->vp9.supported = false; - } - - if(supported_video_codecs_vulkan) { - if(!get_ffmpeg_video_codec(GSR_VIDEO_CODEC_H264_VULKAN, vendor)) { - supported_video_codecs_vulkan->h264.supported = false; - } - - if(!get_ffmpeg_video_codec(GSR_VIDEO_CODEC_HEVC_VULKAN, vendor)) { - supported_video_codecs_vulkan->hevc.supported = false; - supported_video_codecs_vulkan->hevc_hdr.supported = false; - supported_video_codecs_vulkan->hevc_10bit.supported = false; - } - - if(!get_ffmpeg_video_codec(GSR_VIDEO_CODEC_AV1_VULKAN, vendor)) { - supported_video_codecs_vulkan->av1.supported = false; - supported_video_codecs_vulkan->av1_hdr.supported = false; - supported_video_codecs_vulkan->av1_10bit.supported = false; - } - } -} - -static void list_supported_video_codecs(gsr_egl *egl, bool wayland) { - // Dont clean it up on purpose to increase shutdown speed - gsr_supported_video_codecs supported_video_codecs; - get_supported_video_codecs(egl, GSR_VIDEO_CODEC_H264, false, false, &supported_video_codecs); - - gsr_supported_video_codecs supported_video_codecs_vulkan; - get_supported_video_codecs(egl, GSR_VIDEO_CODEC_H264_VULKAN, false, false, &supported_video_codecs_vulkan); - - set_supported_video_codecs_ffmpeg(&supported_video_codecs, &supported_video_codecs_vulkan, egl->gpu_info.vendor); - - if(supported_video_codecs.h264.supported) - puts("h264"); - if(avcodec_find_encoder_by_name("libx264")) - puts("h264_software"); - if(supported_video_codecs.hevc.supported) - puts("hevc"); - if(supported_video_codecs.hevc_hdr.supported && wayland) - puts("hevc_hdr"); - if(supported_video_codecs.hevc_10bit.supported) - puts("hevc_10bit"); - if(supported_video_codecs.av1.supported) - puts("av1"); - if(supported_video_codecs.av1_hdr.supported && wayland) - puts("av1_hdr"); - if(supported_video_codecs.av1_10bit.supported) - puts("av1_10bit"); - if(supported_video_codecs.vp8.supported) - puts("vp8"); - if(supported_video_codecs.vp9.supported) - puts("vp9"); - if(supported_video_codecs_vulkan.h264.supported) - puts("h264_vulkan"); - if(supported_video_codecs_vulkan.hevc.supported) - puts("hevc_vulkan"); - if(supported_video_codecs_vulkan.hevc_hdr.supported && wayland) - puts("hevc_hdr_vulkan"); - if(supported_video_codecs_vulkan.hevc_10bit.supported) - puts("hevc_10bit_vulkan"); - if(supported_video_codecs_vulkan.av1.supported) - puts("av1_vulkan"); - if(supported_video_codecs_vulkan.av1_hdr.supported && wayland) - puts("av1_hdr_vulkan"); - if(supported_video_codecs_vulkan.av1_10bit.supported) - puts("av1_10bit_vulkan"); -} - -static bool monitor_capture_use_drm(const gsr_window *window, gsr_gpu_vendor vendor) { - return gsr_window_get_display_server(window) == GSR_DISPLAY_SERVER_WAYLAND || vendor != GSR_GPU_VENDOR_NVIDIA; -} - -typedef struct { - const gsr_window *window; - int num_monitors; -} capture_options_callback; - -static void output_monitor_info(const gsr_monitor *monitor, void *userdata) { - capture_options_callback *options = (capture_options_callback*)userdata; - if(gsr_window_get_display_server(options->window) == GSR_DISPLAY_SERVER_WAYLAND) { - vec2i monitor_size = monitor->size; - gsr_monitor_rotation monitor_rotation = GSR_MONITOR_ROT_0; - vec2i monitor_position = {0, 0}; - drm_monitor_get_display_server_data(options->window, monitor, &monitor_rotation, &monitor_position); - if(monitor_rotation == GSR_MONITOR_ROT_90 || monitor_rotation == GSR_MONITOR_ROT_270) - std::swap(monitor_size.x, monitor_size.y); - printf("%.*s|%dx%d\n", monitor->name_len, monitor->name, monitor_size.x, monitor_size.y); - } else { - printf("%.*s|%dx%d\n", monitor->name_len, monitor->name, monitor->size.x, monitor->size.y); - } - ++options->num_monitors; -} - -static void camera_query_callback(const char *path, const gsr_capture_v4l2_supported_setup *setup, void *userdata) { - (void)userdata; - printf("%s|%ux%u@%uhz|%s\n", path, setup->resolution.width, setup->resolution.height, gsr_capture_v4l2_framerate_to_number(setup->framerate), gsr_capture_v4l2_pixfmt_to_string(setup->pixfmt)); -} - -// Returns the number of monitors found -static int list_monitors(const gsr_window *window, const char *card_path) { - capture_options_callback options; - options.window = window; - options.num_monitors = 0; - - const bool is_x11 = gsr_window_get_display_server(window) == GSR_DISPLAY_SERVER_X11; - const gsr_connection_type connection_type = is_x11 ? GSR_CONNECTION_X11 : GSR_CONNECTION_DRM; - for_each_active_monitor_output(window, card_path, connection_type, output_monitor_info, &options); - - return options.num_monitors; -} - -static void list_supported_capture_options(const gsr_window *window, const char *card_path, bool do_list_monitors) { - const bool wayland = gsr_window_get_display_server(window) == GSR_DISPLAY_SERVER_WAYLAND; - if(!wayland) { - puts("window"); - puts("focused"); - } - - int num_monitors = 0; - if(do_list_monitors) - num_monitors = list_monitors(window, card_path); - - if(num_monitors > 0) - puts("region"); - - gsr_capture_v4l2_list_devices(camera_query_callback, NULL); - -#ifdef GSR_PORTAL - // Desktop portal capture on x11 doesn't seem to be hardware accelerated - if(!wayland) - return; - - gsr_dbus dbus; - if(!gsr_dbus_init(&dbus, NULL)) - return; - - char *session_handle = NULL; - if(gsr_dbus_screencast_create_session(&dbus, &session_handle) == 0) - puts("portal"); - - gsr_dbus_deinit(&dbus); -#endif -} - -static void version_command(void *userdata) { - (void)userdata; - puts(GSR_VERSION); - fflush(stdout); - _exit(0); -} - -struct WindowingSetup { - Display *dpy; - gsr_window *window; - gsr_egl egl; - bool list_monitors; -}; - -static WindowingSetup setup_windowing(bool setup_egl) { - WindowingSetup setup; - memset(&setup, 0, sizeof(setup)); - - bool wayland = false; - setup.dpy = XOpenDisplay(nullptr); - if (!setup.dpy) { - wayland = true; - fprintf(stderr, "gsr warning: failed to connect to the X server. Assuming wayland is running without Xwayland\n"); - } - - XSetErrorHandler(x11_error_handler); - XSetIOErrorHandler(x11_io_error_handler); - - if(!wayland) - wayland = is_xwayland(setup.dpy); - - if(!wayland && is_using_prime_run()) { - // Disable prime-run and similar options as it doesn't work, the monitor to capture has to be run on the same device. - // This is fine on wayland since nvidia uses drm interface there and the monitor query checks the monitors connected - // to the drm device. - fprintf(stderr, "gsr warning: use of prime-run on X11 is not supported. Disabling prime-run\n"); - disable_prime_run(); - } - - setup.window = gsr_window_create(setup.dpy, wayland); - if(!setup.window) { - fprintf(stderr, "gsr error: failed to create window\n"); - _exit(1); - } - - setup.list_monitors = true; - - if(setup_egl) { - if(!gsr_egl_load(&setup.egl, setup.window, false, false)) { - fprintf(stderr, "gsr error: failed to load opengl\n"); - _exit(22); - } - - setup.egl.card_path[0] = '\0'; - if(monitor_capture_use_drm(setup.window, setup.egl.gpu_info.vendor)) { - // TODO: Allow specifying another card, and in other places - if(!gsr_get_valid_card_path(&setup.egl, setup.egl.card_path, true)) { - fprintf(stderr, "gsr error: no /dev/dri/cardX device found. Make sure that you have at least one monitor connected\n"); - setup.list_monitors = false; - } - } else { - gsr_get_valid_card_path(&setup.egl, setup.egl.card_path, false); - } - } - - return setup; -} - -static void info_command(void *userdata) { - (void)userdata; - WindowingSetup windowing_setup = setup_windowing(true); - const bool wayland = gsr_window_get_display_server(windowing_setup.window) == GSR_DISPLAY_SERVER_WAYLAND; - - av_log_set_level(AV_LOG_FATAL); - - puts("section=system_info"); - list_system_info(wayland); - if(windowing_setup.egl.gpu_info.is_steam_deck) - puts("is_steam_deck|yes"); - else - puts("is_steam_deck|no"); - printf("gsr_version|%s\n", GSR_VERSION); - puts("section=gpu_info"); - list_gpu_info(&windowing_setup.egl); - puts("section=video_codecs"); - list_supported_video_codecs(&windowing_setup.egl, wayland); - puts("section=image_formats"); - puts("jpeg"); - puts("png"); - puts("section=capture_options"); - list_supported_capture_options(windowing_setup.window, windowing_setup.egl.card_path, windowing_setup.list_monitors); - - fflush(stdout); - - // Not needed as this will just slow down shutdown - //gsr_egl_unload(&egl); - //gsr_window_destroy(&window); - //if(dpy) - // XCloseDisplay(dpy); - - _exit(0); -} - -static void list_audio_devices_command(void *userdata) { - (void)userdata; - const AudioDevices audio_devices = get_pulseaudio_inputs(); - - if(!audio_devices.default_output.empty()) - puts("default_output|Default output"); - - if(!audio_devices.default_input.empty()) - puts("default_input|Default input"); - - for(const auto &audio_input : audio_devices.audio_inputs) { - printf("%s|%s\n", audio_input.name.c_str(), audio_input.description.c_str()); - } - - fflush(stdout); - _exit(0); -} - -static bool app_audio_query_callback(const char *app_name, void*) { - puts(app_name); - return true; -} - -static void list_application_audio_command(void *userdata) { - (void)userdata; -#ifdef GSR_APP_AUDIO - if(pulseaudio_server_is_pipewire()) { - gsr_pipewire_audio audio; - if(gsr_pipewire_audio_init(&audio)) { - gsr_pipewire_audio_for_each_app(&audio, app_audio_query_callback, NULL); - gsr_pipewire_audio_deinit(&audio); - } - } -#endif - - fflush(stdout); - _exit(0); -} - -static void list_v4l2_devices(void *userdata) { - (void)userdata; - gsr_capture_v4l2_list_devices(camera_query_callback, NULL); - - fflush(stdout); - _exit(0); -} - -// |card_path| can be NULL. If not NULL then |vendor| has to be valid -static void list_capture_options_command(const char *card_path, void *userdata) { - (void)userdata; - WindowingSetup windowing_setup = setup_windowing(card_path != nullptr); - - if(card_path) - list_supported_capture_options(windowing_setup.window, card_path, true); - else - list_supported_capture_options(windowing_setup.window, windowing_setup.egl.card_path, windowing_setup.list_monitors); - - fflush(stdout); - - // Not needed as this will just slow down shutdown - //gsr_egl_unload(&egl); - //gsr_window_destroy(&window); - //if(dpy) - // XCloseDisplay(dpy); - - _exit(0); -} - -static void list_monitors_command(void *userdata) { - (void)userdata; - WindowingSetup windowing_setup = setup_windowing(true); - - if(windowing_setup.list_monitors) - list_monitors(windowing_setup.window, windowing_setup.egl.card_path); - - fflush(stdout); - - // Not needed as this will just slow down shutdown - //gsr_egl_unload(&egl); - //gsr_window_destroy(&window); - //if(dpy) - // XCloseDisplay(dpy); - - _exit(0); -} - -static std::string validate_monitor_get_valid(const gsr_egl *egl, const char* window) { - const bool is_x11 = gsr_window_get_display_server(egl->window) == GSR_DISPLAY_SERVER_X11; - const gsr_connection_type connection_type = is_x11 ? GSR_CONNECTION_X11 : GSR_CONNECTION_DRM; - const bool capture_use_drm = monitor_capture_use_drm(egl->window, egl->gpu_info.vendor); - - std::string capture_source_result = window; - if(strcmp(capture_source_result.c_str(), "screen") == 0) { - FirstOutputCallback data; - data.output_name = NULL; - for_each_active_monitor_output(egl->window, egl->card_path, connection_type, get_first_output_callback, &data); - - if(data.output_name) { - capture_source_result = data.output_name; - free(data.output_name); - } else { - fprintf(stderr, "gsr error: no usable output found\n"); - _exit(51); - } - } else if(capture_use_drm || (strcmp(capture_source_result.c_str(), "screen-direct") != 0 && strcmp(capture_source_result.c_str(), "screen-direct-force") != 0)) { - gsr_monitor gmon; - if(!get_monitor_by_name(egl, connection_type, capture_source_result.c_str(), &gmon)) { - fprintf(stderr, "gsr error: display \"%s\" not found, expected one of:\n", capture_source_result.c_str()); - fprintf(stderr, " \"screen\"\n"); - if(!capture_use_drm) - fprintf(stderr, " \"screen-direct\"\n"); - - MonitorOutputCallbackUserdata userdata; - userdata.window = egl->window; - for_each_active_monitor_output(egl->window, egl->card_path, connection_type, monitor_output_callback_print, &userdata); - _exit(51); - } - } - return capture_source_result; -} - -static std::string get_monitor_by_region_center(const gsr_egl *egl, vec2i region_position, vec2i region_size, vec2i *monitor_pos, vec2i *monitor_size, double *monitor_scale_inverted) { - const bool is_x11 = gsr_window_get_display_server(egl->window) == GSR_DISPLAY_SERVER_X11; - const gsr_connection_type connection_type = is_x11 ? GSR_CONNECTION_X11 : GSR_CONNECTION_WAYLAND; - - MonitorByPositionCallback data; - data.window = egl->window; - data.position = { region_position.x + region_size.x / 2, region_position.y + region_size.y / 2 }; - data.output_name = NULL; - data.monitor_pos = {0, 0}; - data.monitor_size = {0, 0}; - data.monitor_scale_inverted = 1.0; - for_each_active_monitor_output(egl->window, egl->card_path, connection_type, get_monitor_by_position_callback, &data); - - std::string result; - if(data.output_name) { - result = data.output_name; - free(data.output_name); - } - *monitor_pos = data.monitor_pos; - *monitor_size = data.monitor_size; - *monitor_scale_inverted = data.monitor_scale_inverted; - return result; -} - -static gsr_kms_client kms_client; -static bool kms_client_initialized = false; -static gsr_kms_response kms_response; -static gsr_kde_night_light *kde_night_light = nullptr; -static bool kde_night_light_initialized = false; - -static gsr_cursor x11_cursor; -static Display *x11_cursor_display = NULL; - -static gsr_capture* create_monitor_capture(const args_parser &arg_parser, gsr_egl *egl, const CaptureSource &capture_source, bool prefer_ximage) { - if(gsr_window_get_display_server(egl->window) == GSR_DISPLAY_SERVER_X11 && prefer_ximage) { - gsr_capture_ximage_params ximage_params; - memset(&ximage_params, 0, sizeof(ximage_params)); - ximage_params.egl = egl; - ximage_params.cursor = &x11_cursor; - ximage_params.display_to_capture = capture_source.name.c_str(); - ximage_params.record_cursor = arg_parser.record_cursor; - ximage_params.output_resolution = arg_parser.output_resolution; - ximage_params.region_size = capture_source.region_size; - ximage_params.region_position = capture_source.region_pos; - return gsr_capture_ximage_create(&ximage_params); - } - - if(monitor_capture_use_drm(egl->window, egl->gpu_info.vendor)) { - if(!kms_client_initialized) { - kms_client_initialized = true; - const int kms_init_res = gsr_kms_client_init(&kms_client, egl->card_path); - if(kms_init_res != 0) - _exit(kms_init_res < 0 ? 1 : kms_init_res); - } - - if(!kde_night_light_initialized && gsr_window_get_display_server(egl->window) == GSR_DISPLAY_SERVER_WAYLAND) { - kde_night_light_initialized = true; - kde_night_light = gsr_kde_night_light_create(); - } - - gsr_capture_kms_params kms_params; - memset(&kms_params, 0, sizeof(kms_params)); - kms_params.egl = egl; - kms_params.x11_cursor = &x11_cursor; - kms_params.kms_response = &kms_response; - kms_params.kde_night_light = kde_night_light; - kms_params.display_to_capture = capture_source.name.c_str(); - kms_params.record_cursor = arg_parser.record_cursor; - kms_params.hdr = video_codec_is_hdr(arg_parser.video_codec); - kms_params.fps = arg_parser.fps; - kms_params.output_resolution = arg_parser.output_resolution; - kms_params.region_size = capture_source.region_size; - kms_params.region_position = capture_source.region_pos; - return gsr_capture_kms_create(&kms_params); - } else { - const char *capture_source_real = capture_source.name.c_str(); - const bool direct_capture = strcmp(capture_source.name.c_str(), "screen-direct") == 0 || strcmp(capture_source.name.c_str(), "screen-direct-force") == 0; - if(direct_capture) { - capture_source_real = "screen"; - fprintf(stderr, "gsr warning: %s capture option is not recommended unless you use G-SYNC as Nvidia has driver issues that can cause your system or games to freeze/crash.\n", capture_source.name.c_str()); - } - - gsr_capture_nvfbc_params nvfbc_params; - memset(&nvfbc_params, 0, sizeof(nvfbc_params)); - nvfbc_params.egl = egl; - nvfbc_params.display_to_capture = capture_source_real; - nvfbc_params.fps = arg_parser.fps; - nvfbc_params.direct_capture = direct_capture; - nvfbc_params.record_cursor = arg_parser.record_cursor; - nvfbc_params.output_resolution = arg_parser.output_resolution; - nvfbc_params.region_size = capture_source.region_size; - nvfbc_params.region_position = capture_source.region_pos; - return gsr_capture_nvfbc_create(&nvfbc_params); - } -} - -static void monitor_output_callback_print_region(const gsr_monitor *monitor, void *userdata) { - const vec2i monitor_position = monitor->logical_pos; - const vec2i monitor_size = monitor->logical_size; - fprintf(stderr, " \"%.*s\" (%dx%d+%d+%d)\n", monitor->name_len, monitor->name, monitor_size.x, monitor_size.y, monitor_position.x, monitor_position.y); -} - -static std::string region_get_data(gsr_egl *egl, vec2i *region_size, vec2i *region_position) { - vec2i monitor_pos = {0, 0}; - vec2i monitor_size = {0, 0}; - double monitor_scale_inverted = 1.0; - std::string window = get_monitor_by_region_center(egl, *region_position, *region_size, &monitor_pos, &monitor_size, &monitor_scale_inverted); - if(window.empty()) { - const bool is_x11 = gsr_window_get_display_server(egl->window) == GSR_DISPLAY_SERVER_X11; - const gsr_connection_type connection_type = is_x11 ? GSR_CONNECTION_X11 : GSR_CONNECTION_WAYLAND; - fprintf(stderr, "gsr error: the region %dx%d+%d+%d doesn't match any monitor. Available monitors and their regions:\n", region_size->x, region_size->y, region_position->x, region_position->y); - - MonitorOutputCallbackUserdata userdata; - userdata.window = egl->window; - for_each_active_monitor_output(egl->window, egl->card_path, connection_type, monitor_output_callback_print_region, &userdata); - _exit(51); - } - - // Capture whole monitor when region size is set to 0x0 - if(region_size->x == 0 && region_size->y == 0) { - region_position->x = 0; - region_position->y = 0; - } else { - region_position->x -= monitor_pos.x; - region_position->y -= monitor_pos.y; - // Match drm plane coordinate space (1x scaling) to wayland coordinate space (which may have scaling set by user) - region_position->x *= monitor_scale_inverted; - region_position->y *= monitor_scale_inverted; - - region_size->x *= monitor_scale_inverted; - region_size->y *= monitor_scale_inverted; - } - return window; -} - -static gsr_capture* create_capture_impl(const args_parser &arg_parser, gsr_egl *egl, CaptureSource &capture_source, bool prefer_ximage) { - bool follow_focused = false; - const bool wayland = gsr_window_get_display_server(egl->window) == GSR_DISPLAY_SERVER_WAYLAND; - - gsr_capture *capture = nullptr; - if(capture_source.type == GSR_CAPTURE_SOURCE_TYPE_FOCUSED_WINDOW) { - if(wayland) { - fprintf(stderr, "gsr error: GPU Screen Recorder window capture only works in a pure X11 session. Xwayland is not supported. You can record a monitor instead on wayland\n"); - _exit(2); - } - - if(arg_parser.output_resolution.x <= 0 || arg_parser.output_resolution.y <= 0) { - fprintf(stderr, "gsr error: invalid value for option -s '%dx%d' when using -w focused option. expected width and height to be greater than 0\n", arg_parser.output_resolution.x, arg_parser.output_resolution.y); - args_parser_print_usage(); - _exit(1); - } - - follow_focused = true; - } else if(capture_source.type == GSR_CAPTURE_SOURCE_TYPE_PORTAL) { -#ifdef GSR_PORTAL - // Desktop portal capture on x11 doesn't seem to be hardware accelerated - if(!wayland) { - fprintf(stderr, "gsr error: desktop portal capture is not supported on X11\n"); - _exit(1); - } - - gsr_capture_portal_params portal_params; - memset(&portal_params, 0, sizeof(portal_params)); - portal_params.egl = egl; - portal_params.record_cursor = arg_parser.record_cursor; - portal_params.restore_portal_session = arg_parser.restore_portal_session; - portal_params.portal_session_token_filepath = arg_parser.portal_session_token_filepath; - portal_params.output_resolution = arg_parser.output_resolution; - capture = gsr_capture_portal_create(&portal_params); - if(!capture) - _exit(1); -#else - fprintf(stderr, "gsr error: option '-w portal' used but GPU Screen Recorder was compiled without desktop portal support. Please recompile GPU Screen recorder with the -Dportal=true option\n"); - _exit(2); -#endif - } else if(capture_source.type == GSR_CAPTURE_SOURCE_TYPE_REGION) { - capture_source.name = region_get_data(egl, &capture_source.region_size, &capture_source.region_pos); - capture = create_monitor_capture(arg_parser, egl, capture_source, prefer_ximage); - if(!capture) - _exit(1); - } else if(capture_source.type == GSR_CAPTURE_SOURCE_TYPE_MONITOR) { - capture_source.name = validate_monitor_get_valid(egl, capture_source.name.c_str()); - capture = create_monitor_capture(arg_parser, egl, capture_source, prefer_ximage); - if(!capture) - _exit(1); - } else if(capture_source.type == GSR_CAPTURE_SOURCE_TYPE_V4L2) { - gsr_capture_v4l2_params v4l2_params; - memset(&v4l2_params, 0, sizeof(v4l2_params)); - v4l2_params.egl = egl; - v4l2_params.output_resolution = arg_parser.output_resolution; - v4l2_params.device_path = capture_source.name.c_str(); - v4l2_params.pixfmt = capture_source.v4l2_pixfmt; - v4l2_params.camera_fps = capture_source.camera_fps; - v4l2_params.camera_resolution.width = capture_source.camera_resolution.x; - v4l2_params.camera_resolution.height = capture_source.camera_resolution.y; - capture = gsr_capture_v4l2_create(&v4l2_params); - if(!capture) - _exit(1); - } else { - if(wayland) { - fprintf(stderr, "gsr error: GPU Screen Recorder window capture only works in a pure X11 session. Xwayland is not supported. You can record a monitor instead on wayland or use -w portal option which supports window capture if your wayland compositor supports window capture\n"); - _exit(2); - } - } - - if(!capture) { - gsr_capture_xcomposite_params xcomposite_params; - memset(&xcomposite_params, 0, sizeof(xcomposite_params)); - xcomposite_params.egl = egl; - xcomposite_params.cursor = &x11_cursor; - xcomposite_params.window = capture_source.window_id; - xcomposite_params.follow_focused = follow_focused; - xcomposite_params.record_cursor = arg_parser.record_cursor; - xcomposite_params.output_resolution = arg_parser.output_resolution; - capture = gsr_capture_xcomposite_create(&xcomposite_params); - if(!capture) - _exit(1); - } - - return capture; -} - -static gsr_color_range image_format_to_color_range(gsr_image_format image_format, int image_quality) { - switch(image_format) { - case GSR_IMAGE_FORMAT_JPEG: return image_quality >= JPEG_YUV444_QUALITY_THRESHOLD ? GSR_COLOR_RANGE_FULL : GSR_COLOR_RANGE_LIMITED; - case GSR_IMAGE_FORMAT_PNG: return GSR_COLOR_RANGE_FULL; - } - assert(false); - return GSR_COLOR_RANGE_FULL; -} - -static int video_quality_to_image_quality_value(gsr_video_quality video_quality) { - switch(video_quality) { - case GSR_VIDEO_QUALITY_MEDIUM: - return 75; - case GSR_VIDEO_QUALITY_HIGH: - return 85; - case GSR_VIDEO_QUALITY_VERY_HIGH: - return JPEG_YUV444_QUALITY_THRESHOLD; // Quality above 90 makes the jpeg image encoder (stb_image_writer) use yuv444 instead of yuv420, which greatly improves small colored text quality on dark background - case GSR_VIDEO_QUALITY_ULTRA: - return 97; - } - assert(false); - return 90; -} - -static bool any_video_sources_uses_external_image(std::vector<VideoSource> &video_sources) { - for(VideoSource &video_source : video_sources) { - if(gsr_capture_uses_external_image(video_source.capture)) - return true; - } - return false; -} - -static std::vector<VideoSource> create_video_sources(const args_parser &arg_parser, gsr_egl *egl, bool prefer_ximage, std::vector<CaptureSource> &capture_sources, vec2i &video_size) { - std::vector<VideoSource> video_sources; - video_sources.reserve(capture_sources.size()); - - for(CaptureSource &capture_source : capture_sources) { - gsr_capture_metadata capture_metadata; - memset(&capture_metadata, 0, sizeof(capture_metadata)); - capture_metadata.fps = arg_parser.fps; - capture_metadata.halign = capture_source.halign; - capture_metadata.valign = capture_source.valign; - capture_metadata.flip = (gsr_flip)capture_source.flip; - video_sources.push_back(VideoSource{create_capture_impl(arg_parser, egl, capture_source, prefer_ximage), capture_metadata, &capture_source}); - } - - for(VideoSource &video_source : video_sources) { - int capture_result = gsr_capture_start(video_source.capture, &video_source.metadata); - if(capture_result != 0) { - fprintf(stderr, "gsr error: gsr_capture_start failed\n"); - _exit(capture_result); - } - } - - vec2i start_pos = {99999, 99999}; - vec2i end_pos = {-99999, -99999}; - for(const VideoSource &video_source : video_sources) { - // TODO: Skip scalar positions for now, but this should be handled in a better way. - // Maybe handle scalars at the next loop by multiplying video size by the scalar. - if(video_source.capture_source->pos.x_type == VVEC2I_TYPE_SCALAR || video_source.capture_source->pos.y_type == VVEC2I_TYPE_SCALAR - || (video_source.capture_source->size.x_type == VVEC2I_TYPE_SCALAR && video_source.capture_source->size.x != 100) - || (video_source.capture_source->size.y_type == VVEC2I_TYPE_SCALAR && video_source.capture_source->size.y != 100)) - { - continue; - } - const vec2i video_source_start_pos = {video_source.capture_source->pos.x, video_source.capture_source->pos.y}; - const vec2i video_source_end_pos = {video_source_start_pos.x + video_source.metadata.video_size.x, video_source_start_pos.y + video_source.metadata.video_size.y}; - - start_pos.x = std::min(start_pos.x, video_source_start_pos.x); - start_pos.y = std::min(start_pos.y, video_source_start_pos.y); - - end_pos.x = std::max(end_pos.x, video_source_end_pos.x); - end_pos.y = std::max(end_pos.y, video_source_end_pos.y); - } - - video_size.x = std::max(0, end_pos.x - start_pos.x); - video_size.y = std::max(0, end_pos.y - start_pos.y); - - for(VideoSource &video_source : video_sources) { - video_source.metadata.video_size = video_size; - } - - return video_sources; -} - -static void video_sources_update_with_real_video_size(std::vector<CaptureSource> &capture_sources, std::vector<VideoSource> &video_sources, vec2i video_size) { - assert(capture_sources.size() == video_sources.size()); - for(size_t i = 0; i < capture_sources.size(); ++i) { - CaptureSource &capture_source = capture_sources[i]; - VideoSource &video_source = video_sources[i]; - - video_source.metadata.recording_size = video_source.metadata.video_size; - // TODO: What if this updated resolution is above max resolution? - video_source.metadata.video_size = video_size; - - if(capture_source.pos.x != 0 || capture_source.pos.y != 0) { - video_source.metadata.position.x = capture_source.pos.x; - video_source.metadata.position.y = capture_source.pos.y; - - if(capture_source.pos.x_type == VVEC2I_TYPE_SCALAR) - video_source.metadata.position.x = video_source.metadata.video_size.x * ((double)video_source.metadata.position.x / 100.0); - - if(capture_source.pos.y_type == VVEC2I_TYPE_SCALAR) - video_source.metadata.position.y = video_source.metadata.video_size.y * ((double)video_source.metadata.position.y / 100.0); - } - - if(capture_source.size.x != 0 || capture_source.size.y != 0) { - video_source.metadata.recording_size.x = capture_source.size.x; - video_source.metadata.recording_size.y = capture_source.size.y; - - if(capture_source.size.x_type == VVEC2I_TYPE_SCALAR) - video_source.metadata.recording_size.x = video_source.metadata.video_size.x * ((double)video_source.metadata.recording_size.x / 100.0); - - if(capture_source.size.y_type == VVEC2I_TYPE_SCALAR) - video_source.metadata.recording_size.y = video_source.metadata.video_size.y * ((double)video_source.metadata.recording_size.y / 100.0); - } - } -} - -static void gsr_capture_kms_cleanup_kms_fds() { - for(int i = 0; i < kms_response.num_items; ++i) { - for(int j = 0; j < kms_response.items[i].num_dma_bufs; ++j) { - gsr_kms_response_dma_buf *dma_buf = &kms_response.items[i].dma_buf[j]; - if(dma_buf->fd > 0) { - close(dma_buf->fd); - dma_buf->fd = -1; - } - } - kms_response.items[i].num_dma_bufs = 0; - } - kms_response.num_items = 0; -} - -static void load_plugins(gsr_plugins *plugins, args_parser &arg_parser, gsr_egl *egl, vec2i video_size) { - const Arg *plugin_arg = args_parser_get_arg(&arg_parser, "-p"); - assert(plugin_arg); - - if(plugin_arg->num_values > 0) { - const gsr_color_depth color_depth = video_codec_to_bit_depth(arg_parser.video_codec); - assert(color_depth == GSR_COLOR_DEPTH_8_BITS || color_depth == GSR_COLOR_DEPTH_10_BITS); - - const gsr_plugin_init_params plugin_init_params = { - (unsigned int)video_size.x, - (unsigned int)video_size.y, - (unsigned int)arg_parser.fps, - color_depth == GSR_COLOR_DEPTH_8_BITS ? GSR_PLUGIN_COLOR_DEPTH_8_BITS : GSR_PLUGIN_COLOR_DEPTH_10_BITS, - egl->context_type == GSR_GL_CONTEXT_TYPE_GLX ? GSR_PLUGIN_GRAPHICS_API_GLX : GSR_PLUGIN_GRAPHICS_API_EGL_ES, - }; - - if(!gsr_plugins_init(plugins, plugin_init_params, egl)) - _exit(1); - - for(int i = 0; i < plugin_arg->num_values; ++i) { - if(!gsr_plugins_load_plugin(plugins, plugin_arg->values[i])) - _exit(1); - } - } -} - -// TODO: 10-bit and hdr. -static void capture_image_to_file(args_parser &arg_parser, gsr_egl *egl, gsr_window *window, gsr_image_format image_format, std::vector<CaptureSource> &capture_sources) { - const int image_quality = video_quality_to_image_quality_value(arg_parser.video_quality); - const gsr_color_range color_range = image_format_to_color_range(image_format, image_quality); - arg_parser.fps = 60; // We want to capture an image as soon as possible - - vec2i video_size = {0, 0}; - std::vector<VideoSource> video_sources = create_video_sources(arg_parser, egl, true, capture_sources, video_size); - video_sources_update_with_real_video_size(capture_sources, video_sources, video_size); - - const Arg *plugin_arg = args_parser_get_arg(&arg_parser, "-p"); - assert(plugin_arg); - - gsr_plugins plugins; - memset(&plugins, 0, sizeof(plugins)); - - if(plugin_arg->num_values > 0) - load_plugins(&plugins, arg_parser, egl, video_size); - - gsr_image_writer image_writer; - if(!gsr_image_writer_init_opengl(&image_writer, egl, video_size.x, video_size.y)) { - fprintf(stderr, "gsr error: capture_image_to_file: gsr_image_write_gl_init failed\n"); - _exit(1); - } - - gsr_color_conversion_params color_conversion_params; - memset(&color_conversion_params, 0, sizeof(color_conversion_params)); - color_conversion_params.color_range = color_range; - color_conversion_params.egl = egl; - color_conversion_params.load_external_image_shader = any_video_sources_uses_external_image(video_sources); - - color_conversion_params.destination_textures[0] = image_writer.texture; - color_conversion_params.destination_textures_size[0] = video_size; - color_conversion_params.num_destination_textures = 1; - color_conversion_params.destination_color = GSR_DESTINATION_COLOR_RGB; - - gsr_color_conversion color_conversion; - if(gsr_color_conversion_init(&color_conversion, &color_conversion_params) != 0) { - fprintf(stderr, "gsr error: capture_image_to_file: failed to create color conversion\n"); - _exit(1); - } - - gsr_color_conversion_clear(&color_conversion); - - gsr_color_conversion *output_color_conversion = plugins.num_plugins > 0 ? &plugins.color_conversion : &color_conversion; - - bool should_stop_error = false; - egl->glClear(0); - - while(running) { - while(gsr_window_process_event(window)) { - if(x11_cursor_display && arg_parser.record_cursor) - gsr_cursor_on_event(&x11_cursor, gsr_window_get_event_data(window)); - - for(VideoSource &video_source : video_sources) { - gsr_capture_on_event(video_source.capture, egl); - } - } - - if(x11_cursor_display && arg_parser.record_cursor) - gsr_cursor_tick(&x11_cursor, DefaultRootWindow(x11_cursor_display)); - - gsr_capture_kms_cleanup_kms_fds(); - - if(kms_client_initialized) { - if(gsr_kms_client_get_kms(&kms_client, &kms_response) != 0) - fprintf(stderr, "gsr error: failed to get kms, error: %d (%s)\n", kms_response.result, kms_response.err_msg); - } - - should_stop_error = false; - for(VideoSource &video_source : video_sources) { - gsr_capture_tick(video_source.capture); - if(gsr_capture_should_stop(video_source.capture, &should_stop_error)) { - running = 0; - break; - } - } - - for(VideoSource &video_source : video_sources) { - if(video_source.capture->pre_capture) - video_source.capture->pre_capture(video_source.capture, &video_source.metadata, output_color_conversion); - } - - if(output_color_conversion->schedule_clear) { - output_color_conversion->schedule_clear = false; - gsr_color_conversion_clear(output_color_conversion); - } - - bool all_sources_captured = true; - for(VideoSource &video_source : video_sources) { - // It can fail, for example when capturing portal and the target is a monitor that hasn't been updated. - // This can also happen for example if the system suspends and the monitor to capture's framebuffer is gone, or if the target window disappeared. - if(gsr_capture_capture(video_source.capture, &video_source.metadata, output_color_conversion) != 0) - all_sources_captured = false; - } - - gsr_capture_kms_cleanup_kms_fds(); - - if(all_sources_captured) - break; - - if(running) - usleep(30 * 1000); // 30 ms - } - - if(plugins.num_plugins > 0) { - gsr_plugins_draw(&plugins); - gsr_color_conversion_draw(&color_conversion, plugins.texture, - {0, 0}, video_size, - {0, 0}, video_size, - video_size, GSR_ROT_0, GSR_FLIP_NONE, GSR_SOURCE_COLOR_RGB, false); - } - - gsr_egl_swap_buffers(egl); - - if(!should_stop_error) { - if(!gsr_image_writer_write_to_file(&image_writer, arg_parser.filename, image_format, image_quality)) { - fprintf(stderr, "gsr error: capture_image_to_file: failed to write opengl texture to image output file %s\n", arg_parser.filename); - _exit(1); - } - - if(arg_parser.recording_saved_script) - run_recording_saved_script_async(arg_parser.recording_saved_script, arg_parser.filename, "screenshot"); - } - - gsr_plugins_deinit(&plugins); - gsr_image_writer_deinit(&image_writer); - for(VideoSource &video_source : video_sources) { - gsr_capture_destroy(video_source.capture); - } - _exit(should_stop_error ? 3 : 0); -} - -static AVPixelFormat get_pixel_format(gsr_video_codec video_codec, gsr_gpu_vendor vendor, bool use_software_video_encoder) { - if(use_software_video_encoder) { - return AV_PIX_FMT_NV12; - } else { - if(video_codec_is_vulkan(video_codec)) - return AV_PIX_FMT_VULKAN; - else - return vendor == GSR_GPU_VENDOR_NVIDIA ? AV_PIX_FMT_CUDA : AV_PIX_FMT_VAAPI; - } -} - -static void match_app_audio_input_to_available_apps(const std::vector<AudioInput> &requested_audio_inputs, const std::vector<std::string> &app_audio_names) { - for(const AudioInput &request_audio_input : requested_audio_inputs) { - if(request_audio_input.type != AudioInputType::APPLICATION || request_audio_input.inverted) - continue; - - bool match = false; - for(const std::string &app_name : app_audio_names) { - if(strcasecmp(app_name.c_str(), request_audio_input.name.c_str()) == 0) { - match = true; - break; - } - } - - if(!match) { - fprintf(stderr, "gsr warning: no audio application with the name \"%s\" was found, expected one of the following:\n", request_audio_input.name.c_str()); - for(const std::string &app_name : app_audio_names) { - fprintf(stderr, " * %s\n", app_name.c_str()); - } - fprintf(stderr, " assuming this is intentional (if you are trying to record audio for applications that haven't started yet).\n"); - } - } -} - -struct AudioTrackDescription { - std::optional<std::string> custom_name; - std::vector<std::string> devices; - std::vector<std::string> applications; - bool app_inverse = false; - - std::string to_title() const { - if(custom_name.has_value()){ - return custom_name.value(); - } - std::string title; - if(!devices.empty()) { - title += "Devices: "; - for(size_t i = 0; i < devices.size(); ++i) { - if(i > 0) - title += ", "; - title += devices[i]; - } - } - - if(!applications.empty()) { - if(!title.empty()) - title += ". "; - - if(app_inverse) - title += "All applications except: "; - else - title += "Applications: "; - - for(size_t i = 0; i < applications.size(); ++i) { - if(i > 0) - title += ", "; - title += applications[i]; - } - } - return title; - } -}; - -// Manually check if the audio inputs we give exist. This is only needed for pipewire, not pulseaudio. -// Pipewire instead DEFAULTS TO THE DEFAULT AUDIO INPUT. THAT'S RETARDED. -// OH, YOU MISSPELLED THE AUDIO INPUT? FUCK YOU -static std::vector<MergedAudioInputs> parse_audio_inputs(const AudioDevices &audio_devices, const Arg *audio_input_arg) { - std::vector<MergedAudioInputs> requested_audio_inputs; - - for(int i = 0; i < audio_input_arg->num_values; ++i) { - const char *audio_input = audio_input_arg->values[i]; - if(!audio_input || audio_input[0] == '\0') - continue; - - MergedAudioInputs merged_inputs = parse_audio_input_arg(audio_input); - requested_audio_inputs.push_back(merged_inputs); - AudioTrackDescription audio_track_description; - if(merged_inputs.custom_name.has_value()){ - audio_track_description.custom_name = merged_inputs.custom_name; - } - - for(AudioInput &request_audio_input : requested_audio_inputs.back().audio_inputs) { - if(request_audio_input.type == AudioInputType::APPLICATION) { - audio_track_description.applications.push_back(request_audio_input.name); - audio_track_description.app_inverse = request_audio_input.inverted; - continue; - } - - bool match = false; - - if(request_audio_input.name == "default_output") { - if(audio_devices.default_output.empty()) { - fprintf(stderr, "gsr error: -a default_output was specified but no default audio output is specified in the audio server\n"); - _exit(2); - } - match = true; - audio_track_description.devices.push_back("Default output"); - } else if(request_audio_input.name == "default_input") { - if(audio_devices.default_input.empty()) { - fprintf(stderr, "gsr error: -a default_input was specified but no default audio input is specified in the audio server\n"); - _exit(2); - } - match = true; - audio_track_description.devices.push_back("Default input"); - } else { - const AudioDevice *audio_device = get_audio_device_by_name(audio_devices.audio_inputs, request_audio_input.name.c_str()); - if(audio_device) { - match = true; - audio_track_description.devices.push_back(audio_device->description); - } - } - - if(!match) { - fprintf(stderr, "gsr error: Audio device '%s' is not a valid audio device, expected one of:\n", request_audio_input.name.c_str()); - if(!audio_devices.default_output.empty()) - fprintf(stderr, " default_output (Default output)\n"); - if(!audio_devices.default_input.empty()) - fprintf(stderr, " default_input (Default input)\n"); - for(const auto &audio_device_input : audio_devices.audio_inputs) { - fprintf(stderr, " %s (%s)\n", audio_device_input.name.c_str(), audio_device_input.description.c_str()); - } - _exit(50); - } - } - - requested_audio_inputs.back().track_name = audio_track_description.to_title(); - } - - return requested_audio_inputs; -} - -static bool is_hex_num(char c) { - return (c >= 'A' && c <= 'F') || (c >= 'a' && c <= 'f') || (c >= '0' && c <= '9'); -} - -static bool contains_non_hex_number(const char *str) { - bool hex_start = false; - size_t len = strlen(str); - if(len >= 2 && memcmp(str, "0x", 2) == 0) { - str += 2; - len -= 2; - hex_start = true; - } - - bool is_hex = false; - for(size_t i = 0; i < len; ++i) { - char c = str[i]; - if(c == '\0') - return false; - if(!is_hex_num(c)) - return true; - if((c >= 'A' && c <= 'F') || (c >= 'a' && c <= 'f')) - is_hex = true; - } - - return is_hex && !hex_start; -} - -template <typename T> -static bool string_to_int(const char *str, size_t len, T *number) { - char number_str[32]; - snprintf(number_str, sizeof(number_str), "%.*s", (int)len, str); - - errno = 0; - *number = strtol(number_str, NULL, 0); - return errno == 0; -} - -static void capture_source_type_from_string(const char *capture_source_str, size_t size, CaptureSource &capture_source) { - char capture_source_str_n[64]; - snprintf(capture_source_str_n, sizeof(capture_source_str_n), "%.*s", (int)size, capture_source_str); - - if(size == 7 && memcmp(capture_source_str_n, "focused", 7) == 0) { - capture_source.type = GSR_CAPTURE_SOURCE_TYPE_FOCUSED_WINDOW; - } else if(size == 6 && memcmp(capture_source_str_n, "portal", 6) == 0) { - capture_source.type = GSR_CAPTURE_SOURCE_TYPE_PORTAL; - } else if(size == 6 && memcmp(capture_source_str_n, "region", 6) == 0) { - capture_source.type = GSR_CAPTURE_SOURCE_TYPE_REGION; - } else if(size >= 10 && memcmp(capture_source_str_n, "/dev/video", 10) == 0) { - capture_source.type = GSR_CAPTURE_SOURCE_TYPE_V4L2; - } else if(sscanf(capture_source_str_n, "%dx%d+%d+%d", &capture_source.region_size.x, &capture_source.region_size.y, &capture_source.region_pos.x, &capture_source.region_pos.y) == 4) { - capture_source.type = GSR_CAPTURE_SOURCE_TYPE_REGION; - capture_source.region_set = true; - } else if(contains_non_hex_number(capture_source_str_n)) { - capture_source.type = GSR_CAPTURE_SOURCE_TYPE_MONITOR; - } else { - capture_source.type = GSR_CAPTURE_SOURCE_TYPE_WINDOW; - } -} - -static bool string_to_capture_alignment(const char *str, size_t len, gsr_capture_alignment *alignment) { - if(len == 5 && memcmp(str, "start", 5) == 0) { - *alignment = GSR_CAPTURE_ALIGN_START; - return true; - } else if(len == 6 && memcmp(str, "center", 6) == 0) { - *alignment = GSR_CAPTURE_ALIGN_CENTER; - return true; - } else if(len == 3 && memcmp(str, "end", 3) == 0) { - *alignment = GSR_CAPTURE_ALIGN_END; - return true; - } else { - return false; - } -} - -static bool string_to_v4l2_pixfmt(const char *str, size_t len, gsr_capture_v4l2_pixfmt *pixfmt) { - if(len == 4 && memcmp(str, "auto", 4) == 0) { - *pixfmt = GSR_CAPTURE_V4L2_PIXFMT_AUTO; - return true; - } else if(len == 4 && memcmp(str, "yuyv", 4) == 0) { - *pixfmt = GSR_CAPTURE_V4L2_PIXFMT_YUYV; - return true; - } else if(len == 5 && memcmp(str, "mjpeg", 5) == 0) { - *pixfmt = GSR_CAPTURE_V4L2_PIXFMT_MJPEG; - return true; - } else { - return false; - } -} - -static bool string_to_bool(const char *str, size_t len, bool *value) { - if(len == 4 && memcmp(str, "true", 4) == 0) { - *value = true; - return true; - } else if(len == 5 && memcmp(str, "false", 5) == 0) { - *value = false; - return true; - } else { - return false; - } -} - -static void parse_capture_source_options(const std::string &capture_source_str, CaptureSource &capture_source) { - bool is_first_column = true; - - split_string(capture_source_str, ';', [&](const char *sub, size_t size) { - if(size == 0) - return true; - - // First column contains the capture target - if(is_first_column) { - is_first_column = false; - return true; - } - - if(string_starts_with(sub, size, "x=")) { - capture_source.pos.x_type = sub[size - 1] == '%' ? VVEC2I_TYPE_SCALAR : VVEC2I_TYPE_PIXELS; - sub += 2; - size -= 2; - if(!string_to_int(sub, size, &capture_source.pos.x)) { - fprintf(stderr, "gsr error: invalid capture target value for option x: \"%.*s\", expected a number\n", (int)size, sub); - _exit(1); - } - } else if(string_starts_with(sub, size, "y=")) { - capture_source.pos.y_type = sub[size - 1] == '%' ? VVEC2I_TYPE_SCALAR : VVEC2I_TYPE_PIXELS; - sub += 2; - size -= 2; - if(!string_to_int(sub, size, &capture_source.pos.y)) { - fprintf(stderr, "gsr error: invalid capture target value for option y: \"%.*s\", expected a number\n", (int)size, sub); - _exit(1); - } - - } else if(string_starts_with(sub, size, "width=")) { - capture_source.size.x_type = sub[size - 1] == '%' ? VVEC2I_TYPE_SCALAR : VVEC2I_TYPE_PIXELS; - sub += 6; - size -= 6; - if(!string_to_int(sub, size, &capture_source.size.x)) { - fprintf(stderr, "gsr error: invalid capture target value for option width: \"%.*s\", expected a number\n", (int)size, sub); - _exit(1); - } - } else if(string_starts_with(sub, size, "height=")) { - capture_source.size.y_type = sub[size - 1] == '%' ? VVEC2I_TYPE_SCALAR : VVEC2I_TYPE_PIXELS; - sub += 7; - size -= 7; - if(!string_to_int(sub, size, &capture_source.size.y)) { - fprintf(stderr, "gsr error: invalid capture target value for option height: \"%.*s\", expected a number\n", (int)size, sub); - _exit(1); - } - } else if(string_starts_with(sub, size, "halign=")) { - sub += 7; - size -= 7; - if(!string_to_capture_alignment(sub, size, &capture_source.halign)) { - fprintf(stderr, "gsr error: invalid capture target value for option halign: \"%.*s\", expected a \"start\", \"center\" or \"end\"\n", (int)size, sub); - _exit(1); - } - } else if(string_starts_with(sub, size, "valign=")) { - sub += 7; - size -= 7; - if(!string_to_capture_alignment(sub, size, &capture_source.valign)) { - fprintf(stderr, "gsr error: invalid capture target value for option valign: \"%.*s\", expected a \"start\", \"center\" or \"end\"\n", (int)size, sub); - _exit(1); - } - } else if(string_starts_with(sub, size, "pixfmt=")) { - sub += 7; - size -= 7; - if(!string_to_v4l2_pixfmt(sub, size, &capture_source.v4l2_pixfmt)) { - fprintf(stderr, "gsr error: invalid v4l2 pixfmt value for option pixfmt: \"%.*s\", expected a \"auto\", \"yuyv\" or \"mjpeg\"\n", (int)size, sub); - _exit(1); - } - } else if(string_starts_with(sub, size, "hflip=")) { - sub += 6; - size -= 6; - bool hflip = false; - if(!string_to_bool(sub, size, &hflip)) { - fprintf(stderr, "gsr error: invalid bool value for option hflip: \"%.*s\", expected a \"true\" or \"false\"\n", (int)size, sub); - _exit(1); - } - - if(hflip) - capture_source.flip |= GSR_FLIP_HORIZONTAL; - } else if(string_starts_with(sub, size, "vflip=")) { - sub += 6; - size -= 6; - bool vflip = false; - if(!string_to_bool(sub, size, &vflip)) { - fprintf(stderr, "gsr error: invalid bool value for option vflip: \"%.*s\", expected a \"true\" or \"false\"\n", (int)size, sub); - _exit(1); - } - - if(vflip) - capture_source.flip |= GSR_FLIP_VERTICAL; - } else if(string_starts_with(sub, size, "camera_fps=")) { - sub += 11; - size -= 11; - if(!string_to_int(sub, size, &capture_source.camera_fps)) { - fprintf(stderr, "gsr error: invalid capture target value for option camera_fps: \"%.*s\", expected a number\n", (int)size, sub); - _exit(1); - } - } else if(string_starts_with(sub, size, "camera_width=")) { - sub += 13; - size -= 13; - if(!string_to_int(sub, size, &capture_source.camera_resolution.x)) { - fprintf(stderr, "gsr error: invalid capture target value for option camera_width: \"%.*s\", expected a number\n", (int)size, sub); - _exit(1); - } - } else if(string_starts_with(sub, size, "camera_height=")) { - sub += 14; - size -= 14; - if(!string_to_int(sub, size, &capture_source.camera_resolution.y)) { - fprintf(stderr, "gsr error: invalid capture target value for option camera_height: \"%.*s\", expected a number\n", (int)size, sub); - _exit(1); - } - } else { - fprintf(stderr, "gsr error: invalid capture target option \"%.*s\", expected x, y, width, height, halign, valign, pixfmt, hflip, vflip, camera_fps, camera_width or camera_height\n", (int)size, sub); - _exit(1); - } - - return true; - }); -} - -static std::vector<CaptureSource> parse_capture_source_arg(const char *capture_source_arg, const args_parser &arg_parser) { - std::vector<CaptureSource> requested_capture_sources; - const bool has_multiple_capture_sources = strchr(capture_source_arg, '|') != nullptr; - - split_string(capture_source_arg, '|', [&](const char *sub, size_t size) { - if(size == 0) - return true; - - const char *substr_start = sub; - size_t capture_source_size = size; - const char *capture_source_end = (const char*)memchr(sub, ';', size); - if(capture_source_end) - capture_source_size = capture_source_end - sub; - - CaptureSource capture_source; - capture_source.region_pos = arg_parser.region_position; - capture_source.region_size = arg_parser.region_size; - - if(string_starts_with(sub, capture_source_size, "monitor:")) { - capture_source.type = GSR_CAPTURE_SOURCE_TYPE_MONITOR; - sub += 8; - capture_source_size -= 8; - } else if(string_starts_with(sub, capture_source_size, "window:")) { - capture_source.type = GSR_CAPTURE_SOURCE_TYPE_WINDOW; - sub += 7; - capture_source_size -= 7; - } else if(string_starts_with(sub, capture_source_size, "v4l2:")) { - capture_source.type = GSR_CAPTURE_SOURCE_TYPE_V4L2; - sub += 5; - capture_source_size -= 5; - } else { - capture_source_type_from_string(sub, capture_source_size, capture_source); - } - - capture_source.name.assign(sub, capture_source_size); - - if(capture_source.type == GSR_CAPTURE_SOURCE_TYPE_WINDOW) { - if(!string_to_int(capture_source.name.c_str(), capture_source.name.size(), &capture_source.window_id)) { - fprintf(stderr, "gsr error: invalid window number %s\n", capture_source.name.c_str()); - args_parser_print_usage(); - _exit(1); - } - } - - if(has_multiple_capture_sources) { - capture_source.halign = GSR_CAPTURE_ALIGN_START; - capture_source.valign = GSR_CAPTURE_ALIGN_START; - capture_source.pos = {0, 0, VVEC2I_TYPE_PIXELS, VVEC2I_TYPE_PIXELS}; - } - - parse_capture_source_options(std::string(substr_start, size), capture_source); - requested_capture_sources.push_back(capture_source); - return true; - }); - - return requested_capture_sources; -} - -static bool audio_inputs_has_app_audio(const std::vector<AudioInput> &audio_inputs) { - for(const auto &audio_input : audio_inputs) { - if(audio_input.type == AudioInputType::APPLICATION) - return true; - } - return false; -} - -static bool merged_audio_inputs_has_app_audio(const std::vector<MergedAudioInputs> &merged_audio_inputs) { - for(const auto &merged_audio_input : merged_audio_inputs) { - if(audio_inputs_has_app_audio(merged_audio_input.audio_inputs)) - return true; - } - return false; -} - -// Should use amix if more than 1 audio device and 0 application audio, merged -static bool audio_inputs_should_use_amix(const std::vector<AudioInput> &audio_inputs) { - int num_audio_devices = 0; - int num_app_audio = 0; - - for(const auto &audio_input : audio_inputs) { - if(audio_input.type == AudioInputType::DEVICE) - ++num_audio_devices; - else if(audio_input.type == AudioInputType::APPLICATION) - ++num_app_audio; - } - - return num_audio_devices > 1 && num_app_audio == 0; -} - -static bool merged_audio_inputs_should_use_amix(const std::vector<MergedAudioInputs> &merged_audio_inputs) { - for(const auto &merged_audio_input : merged_audio_inputs) { - if(audio_inputs_should_use_amix(merged_audio_input.audio_inputs)) - return true; - } - return false; -} - -static void validate_merged_audio_inputs_app_audio(const std::vector<MergedAudioInputs> &merged_audio_inputs, const std::vector<std::string> &app_audio_names) { - for(const auto &merged_audio_input : merged_audio_inputs) { - int num_app_audio = 0; - int num_app_inverted_audio = 0; - - for(const auto &audio_input : merged_audio_input.audio_inputs) { - if(audio_input.type == AudioInputType::APPLICATION) { - if(audio_input.inverted) - ++num_app_inverted_audio; - else - ++num_app_audio; - } - } - - match_app_audio_input_to_available_apps(merged_audio_input.audio_inputs, app_audio_names); - - if(num_app_audio > 0 && num_app_inverted_audio > 0) { - fprintf(stderr, "gsr error: argument -a was provided with both app: and app-inverse:, only one of them can be used for one audio track\n"); - _exit(2); - } - } -} - -static gsr_audio_codec select_audio_codec_with_fallback(gsr_audio_codec audio_codec, const std::string &file_extension, bool uses_amix) { - switch(audio_codec) { - case GSR_AUDIO_CODEC_AAC: { - if(file_extension == "webm") { - //audio_codec_to_use = "opus"; - audio_codec = GSR_AUDIO_CODEC_OPUS; - fprintf(stderr, "gsr warning: .webm files only support opus audio codec, changing audio codec from aac to opus\n"); - } - break; - } - case GSR_AUDIO_CODEC_OPUS: { - if(file_extension != "mp4" && file_extension != "mkv" && file_extension != "webm" && file_extension != "ts" && file_extension != "whip") { - //audio_codec_to_use = "aac"; - audio_codec = GSR_AUDIO_CODEC_AAC; - fprintf(stderr, "gsr warning: opus audio codec is only supported by .mp4, .mkv, .webm and .ts files, falling back to aac instead\n"); - } - break; - } - case GSR_AUDIO_CODEC_FLAC: { - // TODO: Also check mpegts? - if(file_extension == "webm") { - //audio_codec_to_use = "opus"; - audio_codec = GSR_AUDIO_CODEC_OPUS; - fprintf(stderr, "gsr warning: .webm files only support opus audio codec, changing audio codec from flac to opus\n"); - } else if(file_extension != "mp4" && file_extension != "mkv") { - //audio_codec_to_use = "aac"; - audio_codec = GSR_AUDIO_CODEC_AAC; - fprintf(stderr, "gsr warning: flac audio codec is only supported by .mp4 and .mkv files, falling back to aac instead\n"); - } else if(uses_amix) { - // TODO: remove this? is it true anymore? - //audio_codec_to_use = "opus"; - audio_codec = GSR_AUDIO_CODEC_OPUS; - fprintf(stderr, "gsr warning: flac audio codec is not supported when mixing audio sources, falling back to opus instead\n"); - } - break; - } - } - return audio_codec; -} - -static bool video_codec_only_supports_low_power_mode(const gsr_supported_video_codecs &supported_video_codecs, gsr_video_codec video_codec) { - switch(video_codec) { - case GSR_VIDEO_CODEC_H264: return supported_video_codecs.h264.low_power; - case GSR_VIDEO_CODEC_HEVC: return supported_video_codecs.hevc.low_power; - case GSR_VIDEO_CODEC_HEVC_HDR: return supported_video_codecs.hevc_hdr.low_power; - case GSR_VIDEO_CODEC_HEVC_10BIT: return supported_video_codecs.hevc_10bit.low_power; - case GSR_VIDEO_CODEC_AV1: return supported_video_codecs.av1.low_power; - case GSR_VIDEO_CODEC_AV1_HDR: return supported_video_codecs.av1_hdr.low_power; - case GSR_VIDEO_CODEC_AV1_10BIT: return supported_video_codecs.av1_10bit.low_power; - case GSR_VIDEO_CODEC_VP8: return supported_video_codecs.vp8.low_power; - case GSR_VIDEO_CODEC_VP9: return supported_video_codecs.vp9.low_power; - case GSR_VIDEO_CODEC_H264_VULKAN: return supported_video_codecs.h264.low_power; - case GSR_VIDEO_CODEC_HEVC_VULKAN: return supported_video_codecs.hevc.low_power; - case GSR_VIDEO_CODEC_HEVC_HDR_VULKAN: return supported_video_codecs.hevc_hdr.low_power; - case GSR_VIDEO_CODEC_HEVC_10BIT_VULKAN: return supported_video_codecs.hevc_10bit.low_power; - case GSR_VIDEO_CODEC_AV1_VULKAN: return supported_video_codecs.av1.low_power; - case GSR_VIDEO_CODEC_AV1_HDR_VULKAN: return supported_video_codecs.av1_hdr.low_power; - case GSR_VIDEO_CODEC_AV1_10BIT_VULKAN: return supported_video_codecs.av1_10bit.low_power; - } - return false; -} - -static const AVCodec* get_av_codec_if_supported(gsr_video_codec video_codec, gsr_egl *egl, bool use_software_video_encoder, const gsr_supported_video_codecs *supported_video_codecs) { - switch(video_codec) { - case GSR_VIDEO_CODEC_H264: - case GSR_VIDEO_CODEC_H264_VULKAN: { - if(use_software_video_encoder) - return avcodec_find_encoder_by_name("libx264"); - else if(supported_video_codecs->h264.supported) - return get_ffmpeg_video_codec(video_codec, egl->gpu_info.vendor); - break; - } - case GSR_VIDEO_CODEC_HEVC: - case GSR_VIDEO_CODEC_HEVC_VULKAN: { - if(supported_video_codecs->hevc.supported) - return get_ffmpeg_video_codec(video_codec, egl->gpu_info.vendor); - break; - } - case GSR_VIDEO_CODEC_HEVC_HDR: - case GSR_VIDEO_CODEC_HEVC_HDR_VULKAN: { - if(supported_video_codecs->hevc_hdr.supported) - return get_ffmpeg_video_codec(video_codec, egl->gpu_info.vendor); - break; - } - case GSR_VIDEO_CODEC_HEVC_10BIT: - case GSR_VIDEO_CODEC_HEVC_10BIT_VULKAN: { - if(supported_video_codecs->hevc_10bit.supported) - return get_ffmpeg_video_codec(video_codec, egl->gpu_info.vendor); - break; - } - case GSR_VIDEO_CODEC_AV1: - case GSR_VIDEO_CODEC_AV1_VULKAN: { - if(supported_video_codecs->av1.supported) - return get_ffmpeg_video_codec(video_codec, egl->gpu_info.vendor); - break; - } - case GSR_VIDEO_CODEC_AV1_HDR: - case GSR_VIDEO_CODEC_AV1_HDR_VULKAN: { - if(supported_video_codecs->av1_hdr.supported) - return get_ffmpeg_video_codec(video_codec, egl->gpu_info.vendor); - break; - } - case GSR_VIDEO_CODEC_AV1_10BIT: - case GSR_VIDEO_CODEC_AV1_10BIT_VULKAN: { - if(supported_video_codecs->av1_10bit.supported) - return get_ffmpeg_video_codec(video_codec, egl->gpu_info.vendor); - break; - } - case GSR_VIDEO_CODEC_VP8: { - if(supported_video_codecs->vp8.supported) - return get_ffmpeg_video_codec(video_codec, egl->gpu_info.vendor); - break; - } - case GSR_VIDEO_CODEC_VP9: { - if(supported_video_codecs->vp9.supported) - return get_ffmpeg_video_codec(video_codec, egl->gpu_info.vendor); - break; - } - } - return nullptr; -} - -static vec2i codec_get_max_resolution(gsr_video_codec video_codec, bool use_software_video_encoder, const gsr_supported_video_codecs *supported_video_codecs) { - switch(video_codec) { - case GSR_VIDEO_CODEC_H264: - case GSR_VIDEO_CODEC_H264_VULKAN: { - if(use_software_video_encoder) - return {4096, 2304}; - else if(supported_video_codecs->h264.supported) - return supported_video_codecs->h264.max_resolution; - break; - } - case GSR_VIDEO_CODEC_HEVC: - case GSR_VIDEO_CODEC_HEVC_VULKAN: { - if(supported_video_codecs->hevc.supported) - return supported_video_codecs->hevc.max_resolution; - break; - } - case GSR_VIDEO_CODEC_HEVC_HDR: - case GSR_VIDEO_CODEC_HEVC_HDR_VULKAN: { - if(supported_video_codecs->hevc_hdr.supported) - return supported_video_codecs->hevc_hdr.max_resolution; - break; - } - case GSR_VIDEO_CODEC_HEVC_10BIT: - case GSR_VIDEO_CODEC_HEVC_10BIT_VULKAN: { - if(supported_video_codecs->hevc_10bit.supported) - return supported_video_codecs->hevc_10bit.max_resolution; - break; - } - case GSR_VIDEO_CODEC_AV1: - case GSR_VIDEO_CODEC_AV1_VULKAN: { - if(supported_video_codecs->av1.supported) - return supported_video_codecs->av1.max_resolution; - break; - } - case GSR_VIDEO_CODEC_AV1_HDR: - case GSR_VIDEO_CODEC_AV1_HDR_VULKAN: { - if(supported_video_codecs->av1_hdr.supported) - return supported_video_codecs->av1_hdr.max_resolution; - break; - } - case GSR_VIDEO_CODEC_AV1_10BIT: - case GSR_VIDEO_CODEC_AV1_10BIT_VULKAN: { - if(supported_video_codecs->av1_10bit.supported) - return supported_video_codecs->av1_10bit.max_resolution; - break; - } - case GSR_VIDEO_CODEC_VP8: { - if(supported_video_codecs->vp8.supported) - return supported_video_codecs->vp8.max_resolution; - break; - } - case GSR_VIDEO_CODEC_VP9: { - if(supported_video_codecs->vp9.supported) - return supported_video_codecs->vp9.max_resolution; - break; - } - } - return {0, 0}; -} - -static bool codec_supports_resolution(vec2i codec_max_resolution, vec2i capture_resolution) { - if(codec_max_resolution.x == 0 || codec_max_resolution.y == 0) - return true; - return codec_max_resolution.x >= capture_resolution.x && codec_max_resolution.y >= capture_resolution.y; -} - -static void print_codec_error(gsr_video_codec video_codec) { - if(video_codec == (gsr_video_codec)GSR_VIDEO_CODEC_AUTO) - video_codec = GSR_VIDEO_CODEC_H264; - - const char *video_codec_name = video_codec_to_string(video_codec); - fprintf(stderr, "gsr error: your gpu does not support '%s' video codec. If you are sure that your gpu does support '%s' video encoding and you are using an AMD/Intel GPU,\n" - " then make sure you have installed the GPU specific vaapi packages (intel-media-driver, libva-intel-driver, libva-mesa-driver and linux-firmware).\n" - " It's also possible that your distro has disabled hardware accelerated video encoding for '%s' video codec.\n" - " This may be the case on corporate distros such as Manjaro, Fedora or OpenSUSE.\n" - " You can test this by running 'vainfo | grep VAEntrypointEncSlice' to see if it matches any H264/HEVC/AV1/VP8/VP9 profile.\n" - " On such distros, you need to manually install mesa from source to enable H264/HEVC hardware acceleration, or use a more user friendly distro. Alternatively record with AV1 if supported by your GPU.\n" - " You can alternatively use the flatpak version of GPU Screen Recorder (https://flathub.org/apps/com.dec05eba.gpu_screen_recorder) which bypasses system issues with patented H264/HEVC codecs.\n" - " If your GPU doesn't support hardware accelerated video encoding then you can use '-fallback-cpu-encoding yes' option to encode with your cpu instead.\n", video_codec_name, video_codec_name, video_codec_name); -} - -static void force_cpu_encoding(args_parser *args_parser) { - args_parser->video_codec = GSR_VIDEO_CODEC_H264; - args_parser->video_encoder = GSR_VIDEO_ENCODER_HW_CPU; - if(args_parser->bitrate_mode == GSR_BITRATE_MODE_VBR) { - fprintf(stderr, "gsr warning: bitrate mode has been forcefully set to qp because software encoding option doesn't support vbr option\n"); - args_parser->bitrate_mode = GSR_BITRATE_MODE_QP; - } -} - -static const AVCodec* pick_video_codec(gsr_egl *egl, args_parser *args_parser, bool use_fallback_codec, bool *low_power, gsr_supported_video_codecs *supported_video_codecs) { - // TODO: software encoder for hevc, av1, vp8 and vp9 - *low_power = false; - const AVCodec *video_codec_f = get_av_codec_if_supported(args_parser->video_codec, egl, args_parser->video_encoder == GSR_VIDEO_ENCODER_HW_CPU, supported_video_codecs); - - if(!video_codec_f && use_fallback_codec && args_parser->video_encoder != GSR_VIDEO_ENCODER_HW_CPU) { - switch(args_parser->video_codec) { - case GSR_VIDEO_CODEC_H264: { - fprintf(stderr, "gsr error: selected video codec h264 is not supported by your hardware\n"); - if(args_parser->fallback_cpu_encoding) { - fprintf(stderr, "gsr warning: gpu encoding is not available on your system, trying cpu encoding instead because -fallback-cpu-encoding is enabled. Install the proper vaapi drivers on your system (if supported) if you experience performance issues\n"); - force_cpu_encoding(args_parser); - } - break; - } - case GSR_VIDEO_CODEC_HEVC: - case GSR_VIDEO_CODEC_HEVC_HDR: - case GSR_VIDEO_CODEC_HEVC_10BIT: { - fprintf(stderr, "gsr warning: selected video codec hevc is not supported by your hardware, trying h264 instead\n"); - args_parser->video_codec = GSR_VIDEO_CODEC_H264; - return pick_video_codec(egl, args_parser, true, low_power, supported_video_codecs); - } - case GSR_VIDEO_CODEC_AV1: - case GSR_VIDEO_CODEC_AV1_HDR: - case GSR_VIDEO_CODEC_AV1_10BIT: { - fprintf(stderr, "gsr warning: selected video codec av1 is not supported by your hardware, trying h264 instead\n"); - args_parser->video_codec = GSR_VIDEO_CODEC_H264; - return pick_video_codec(egl, args_parser, true, low_power, supported_video_codecs); - } - case GSR_VIDEO_CODEC_VP8: - case GSR_VIDEO_CODEC_VP9: - // TODO: Cant fallback to other codec because webm only supports vp8/vp9 - break; - case GSR_VIDEO_CODEC_H264_VULKAN: { - fprintf(stderr, "gsr warning: selected video codec h264_vulkan is not supported by your hardware, trying h264 instead\n"); - args_parser->video_codec = GSR_VIDEO_CODEC_H264; - // Need to do a query again because this time it's without vulkan - if(!get_supported_video_codecs(egl, args_parser->video_codec, false, true, supported_video_codecs)) { - fprintf(stderr, "gsr error: failed to query for supported video codecs\n"); - print_codec_error(args_parser->video_codec); - _exit(11); - } - return pick_video_codec(egl, args_parser, true, low_power, supported_video_codecs); - } - case GSR_VIDEO_CODEC_HEVC_VULKAN: - case GSR_VIDEO_CODEC_HEVC_HDR_VULKAN: - case GSR_VIDEO_CODEC_HEVC_10BIT_VULKAN: { - fprintf(stderr, "gsr warning: selected video codec hevc_vulkan is not supported by your hardware, trying hevc instead\n"); - args_parser->video_codec = GSR_VIDEO_CODEC_HEVC; - // Need to do a query again because this time it's without vulkan - if(!get_supported_video_codecs(egl, args_parser->video_codec, false, true, supported_video_codecs)) { - fprintf(stderr, "gsr error: failed to query for supported video codecs\n"); - print_codec_error(args_parser->video_codec); - _exit(11); - } - return pick_video_codec(egl, args_parser, true, low_power, supported_video_codecs); - } - case GSR_VIDEO_CODEC_AV1_VULKAN: - case GSR_VIDEO_CODEC_AV1_HDR_VULKAN: - case GSR_VIDEO_CODEC_AV1_10BIT_VULKAN: { - fprintf(stderr, "gsr warning: selected video codec av1_vulkan is not supported by your hardware, trying av1 instead\n"); - args_parser->video_codec = GSR_VIDEO_CODEC_AV1; - // Need to do a query again because this time it's without vulkan - if(!get_supported_video_codecs(egl, args_parser->video_codec, false, true, supported_video_codecs)) { - fprintf(stderr, "gsr error: failed to query for supported video codecs\n"); - print_codec_error(args_parser->video_codec); - _exit(11); - } - return pick_video_codec(egl, args_parser, true, low_power, supported_video_codecs); - } - } - - video_codec_f = get_av_codec_if_supported(args_parser->video_codec, egl, args_parser->video_encoder == GSR_VIDEO_ENCODER_HW_CPU, supported_video_codecs); - } - - if(!video_codec_f) { - print_codec_error(args_parser->video_codec); - _exit(54); - } - - *low_power = video_codec_only_supports_low_power_mode(*supported_video_codecs, args_parser->video_codec); - - return video_codec_f; -} - -/* Returns -1 if none is available */ -static gsr_video_codec select_appropriate_video_codec_automatically(vec2i video_size, const gsr_supported_video_codecs *supported_video_codecs) { - if(supported_video_codecs->h264.supported && codec_supports_resolution(supported_video_codecs->h264.max_resolution, video_size)) { - fprintf(stderr, "gsr info: using h264 encoder because a codec was not specified\n"); - return GSR_VIDEO_CODEC_H264; - } else if(supported_video_codecs->hevc.supported && codec_supports_resolution(supported_video_codecs->hevc.max_resolution, video_size)) { - fprintf(stderr, "gsr info: using hevc encoder because a codec was not specified and h264 supported max resolution (%dx%d) is less than the capture resolution (%dx%d)\n", - supported_video_codecs->h264.max_resolution.x, supported_video_codecs->h264.max_resolution.y, - video_size.x, video_size.y); - return GSR_VIDEO_CODEC_HEVC; - } else if(supported_video_codecs->av1.supported && codec_supports_resolution(supported_video_codecs->av1.max_resolution, video_size)) { - fprintf(stderr, "gsr info: using av1 encoder because a codec was not specified and hevc supported max resolution (%dx%d) is less than the capture resolution (%dx%d)\n", - supported_video_codecs->hevc.max_resolution.x, supported_video_codecs->hevc.max_resolution.y, - video_size.x, video_size.y); - return GSR_VIDEO_CODEC_AV1; - } else { - return (gsr_video_codec)-1; - } -} - -static const AVCodec* select_video_codec_with_fallback(vec2i video_size, args_parser *args_parser, const char *file_extension, gsr_egl *egl, bool *low_power) { - gsr_supported_video_codecs supported_video_codecs_non_vulkan; - get_supported_video_codecs(egl, args_parser->video_codec, args_parser->video_encoder == GSR_VIDEO_ENCODER_HW_CPU, true, &supported_video_codecs_non_vulkan); - - gsr_supported_video_codecs supported_video_codecs_vulkan = supported_video_codecs_non_vulkan; - set_supported_video_codecs_ffmpeg(&supported_video_codecs_non_vulkan, &supported_video_codecs_vulkan, egl->gpu_info.vendor); - - gsr_supported_video_codecs *supported_video_codecs = video_codec_is_vulkan(args_parser->video_codec) - ? &supported_video_codecs_vulkan - : &supported_video_codecs_non_vulkan; - - const bool video_codec_auto = args_parser->video_codec == (gsr_video_codec)GSR_VIDEO_CODEC_AUTO; - if(video_codec_auto) { - if(strcmp(file_extension, "webm") == 0) { - fprintf(stderr, "gsr info: using vp8 encoder because a codec was not specified and the file extension is .webm\n"); - args_parser->video_codec = GSR_VIDEO_CODEC_VP8; - } else if(args_parser->video_encoder == GSR_VIDEO_ENCODER_HW_CPU) { - fprintf(stderr, "gsr info: using h264 encoder because a codec was not specified\n"); - args_parser->video_codec = GSR_VIDEO_CODEC_H264; - } else if(args_parser->video_encoder != GSR_VIDEO_ENCODER_HW_CPU) { - args_parser->video_codec = select_appropriate_video_codec_automatically(video_size, &supported_video_codecs_non_vulkan); - if(args_parser->video_codec == (gsr_video_codec)-1) { - if(args_parser->fallback_cpu_encoding) { - fprintf(stderr, "gsr warning: gpu encoding is not available on your system or your gpu doesn't support recording at the resolution you are trying to record, trying cpu encoding instead because -fallback-cpu-encoding is enabled. Install the proper vaapi drivers on your system (if supported) if you experience performance issues\n"); - force_cpu_encoding(args_parser); - } else { - fprintf(stderr, "gsr error: no video encoder was specified and neither h264, hevc nor av1 are supported on your system or you are trying to capture at a resolution higher than your system supports for each codec.\n"); - fprintf(stderr, " Ensure that you have installed the proper vaapi driver. If your gpu doesn't support video encoding then you can run gpu-screen-recorder with \"-fallback-cpu-encoding yes\" option to use cpu encoding.\n"); - _exit(52); - } - } - } - } - - if(LIBAVFORMAT_VERSION_INT < AV_VERSION_INT(60, 10, 100) && strcmp(file_extension, "flv") == 0) { - if(args_parser->video_codec != GSR_VIDEO_CODEC_H264) { - args_parser->video_codec = GSR_VIDEO_CODEC_H264; - fprintf(stderr, "gsr warning: hevc/av1 is not compatible with flv in your outdated version of ffmpeg, falling back to h264 instead.\n"); - } - } else if(strcmp(file_extension, "m3u8") == 0) { - if(video_codec_is_av1(args_parser->video_codec)) { - args_parser->video_codec = GSR_VIDEO_CODEC_HEVC; - fprintf(stderr, "gsr warning: av1 is not compatible with hls (m3u8), falling back to hevc instead.\n"); - } - } - - const AVCodec *codec = pick_video_codec(egl, args_parser, true, low_power, supported_video_codecs); - - const vec2i codec_max_resolution = codec_get_max_resolution(args_parser->video_codec, args_parser->video_encoder == GSR_VIDEO_ENCODER_HW_CPU, supported_video_codecs); - if(!codec_supports_resolution(codec_max_resolution, video_size)) { - const char *video_codec_name = video_codec_to_string(args_parser->video_codec); - fprintf(stderr, "gsr error: The max resolution for video codec %s is %dx%d while you are trying to capture at resolution %dx%d. Change capture resolution or video codec and try again\n", - video_codec_name, codec_max_resolution.x, codec_max_resolution.y, video_size.x, video_size.y); - _exit(53); - } - - return codec; -} - -static std::vector<AudioDeviceData> create_device_audio_inputs(const std::vector<AudioInput> &audio_inputs, AVCodecContext *audio_codec_context, int num_channels, double num_audio_frames_shift, std::vector<AVFilterContext*> &src_filter_ctx, bool use_amix) { - std::vector<AudioDeviceData> audio_track_audio_devices; - for(size_t i = 0; i < audio_inputs.size(); ++i) { - const auto &audio_input = audio_inputs[i]; - AVFilterContext *src_ctx = nullptr; - if(use_amix) - src_ctx = src_filter_ctx[i]; - - AudioDeviceData audio_device; - audio_device.audio_input = audio_input; - audio_device.src_filter_ctx = src_ctx; - - if(audio_input.name.empty()) { - audio_device.sound_device.handle = NULL; - audio_device.sound_device.frames = 0; - } else { - const std::string description = "gsr-" + audio_input.name; - if(sound_device_get_by_name(&audio_device.sound_device, description.c_str(), audio_input.name.c_str(), description.c_str(), num_channels, audio_codec_context->frame_size, audio_codec_context_get_audio_format(audio_codec_context)) != 0) { - fprintf(stderr, "gsr error: failed to get \"%s\" audio device\n", audio_input.name.c_str()); - _exit(1); - } - } - - audio_device.frame = create_audio_frame(audio_codec_context); - audio_device.frame->pts = -audio_codec_context->frame_size * num_audio_frames_shift; - - audio_track_audio_devices.push_back(std::move(audio_device)); - } - return audio_track_audio_devices; -} - -#ifdef GSR_APP_AUDIO -static AudioDeviceData create_application_audio_audio_input(const MergedAudioInputs &merged_audio_inputs, AVCodecContext *audio_codec_context, int num_channels, double num_audio_frames_shift, gsr_pipewire_audio *pipewire_audio) { - AudioDeviceData audio_device; - audio_device.frame = create_audio_frame(audio_codec_context); - audio_device.frame->pts = -audio_codec_context->frame_size * num_audio_frames_shift; - - char random_str[8]; - if(!generate_random_characters_standard_alphabet(random_str, sizeof(random_str))) { - fprintf(stderr, "gsr error: failed to generate random string\n"); - _exit(1); - } - - std::string combined_sink_name = "gsr-combined-"; - combined_sink_name.append(random_str, sizeof(random_str)); - combined_sink_name += ".monitor"; - - if(sound_device_get_by_name(&audio_device.sound_device, combined_sink_name.c_str(), "", "gpu-screen-recorder", num_channels, audio_codec_context->frame_size, audio_codec_context_get_audio_format(audio_codec_context)) != 0) { - fprintf(stderr, "gsr error: failed to setup audio recording to combined sink\n"); - _exit(1); - } - - std::vector<const char*> audio_devices_sources; - for(const auto &audio_input : merged_audio_inputs.audio_inputs) { - if(audio_input.type == AudioInputType::DEVICE) - audio_devices_sources.push_back(audio_input.name.c_str()); - } - - bool app_audio_inverted = false; - std::vector<const char*> app_names; - for(const auto &audio_input : merged_audio_inputs.audio_inputs) { - if(audio_input.type == AudioInputType::APPLICATION) { - app_names.push_back(audio_input.name.c_str()); - app_audio_inverted = audio_input.inverted; - } - } - - if(!audio_devices_sources.empty()) { - if(!gsr_pipewire_audio_add_link_from_sources_to_stream(pipewire_audio, audio_devices_sources.data(), audio_devices_sources.size(), combined_sink_name.c_str())) { - fprintf(stderr, "gsr error: failed to add application audio link\n"); - _exit(1); - } - } - - if(app_audio_inverted) { - if(!gsr_pipewire_audio_add_link_from_apps_to_stream_inverted(pipewire_audio, app_names.data(), app_names.size(), combined_sink_name.c_str())) { - fprintf(stderr, "gsr error: failed to add application audio link\n"); - _exit(1); - } - } else { - if(!gsr_pipewire_audio_add_link_from_apps_to_stream(pipewire_audio, app_names.data(), app_names.size(), combined_sink_name.c_str())) { - fprintf(stderr, "gsr error: failed to add application audio link\n"); - _exit(1); - } - } - - return audio_device; -} -#endif - -static bool get_image_format_from_filename(const char *filename, gsr_image_format *image_format) { - if(string_ends_with(filename, ".jpg") || string_ends_with(filename, ".jpeg")) { - *image_format = GSR_IMAGE_FORMAT_JPEG; - return true; - } else if(string_ends_with(filename, ".png")) { - *image_format = GSR_IMAGE_FORMAT_PNG; - return true; - } else { - return false; - } -} - -static void av_write_header(AVFormatContext *av_format_context, const char *ffmpeg_opts) { - AVDictionary *options = nullptr; - av_dict_set(&options, "strict", "experimental", 0); - - if(ffmpeg_opts) - av_dict_parse_string(&options, ffmpeg_opts, "=", ";", 0); - - const int ret = avformat_write_header(av_format_context, &options); - if(ret < 0) - fprintf(stderr, "Error occurred when writing header to output file: %s\n", av_error_to_string(ret)); - - av_dict_free(&options); -} - -static int audio_codec_get_frame_size(gsr_audio_codec audio_codec) { - switch(audio_codec) { - case GSR_AUDIO_CODEC_AAC: return 1024; - case GSR_AUDIO_CODEC_OPUS: return 960; - case GSR_AUDIO_CODEC_FLAC: - assert(false); - return 1024; - } - assert(false); - return 1024; -} - -static size_t calculate_estimated_replay_buffer_packets(int64_t replay_buffer_size_secs, int fps, gsr_audio_codec audio_codec, const std::vector<MergedAudioInputs> &audio_inputs) { - if(replay_buffer_size_secs == -1) - return 0; - - int audio_fps = 0; - if(!audio_inputs.empty()) - audio_fps = AUDIO_SAMPLE_RATE / audio_codec_get_frame_size(audio_codec); - - return replay_buffer_size_secs * (fps + audio_fps * audio_inputs.size()); -} - -static void set_display_server_environment_variables() { - // Some users dont have properly setup environments (no display manager that does systemctl --user import-environment DISPLAY WAYLAND_DISPLAY) - const char *display = getenv("DISPLAY"); - if(!display) { - display = ":0"; - setenv("DISPLAY", display, true); - } - - const char *wayland_display = getenv("WAYLAND_DISPLAY"); - if(!wayland_display) { - wayland_display = "wayland-0"; - setenv("WAYLAND_DISPLAY", wayland_display, true); - } -} - -static bool is_capturing_damage_tracked_target(const std::vector<CaptureSource> &capture_sources) { - for(const CaptureSource &capture_source : capture_sources) { - if(capture_source.type != GSR_CAPTURE_SOURCE_TYPE_V4L2) - return true; - } - return false; -} - -static bool is_capturing_type(const std::vector<CaptureSource> &capture_sources, CaptureSourceType target_type) { - for(const CaptureSource &capture_source : capture_sources) { - if(capture_source.type == target_type) - return true; - } - return false; -} - -static bool has_capture_source_with_region_set(const std::vector<CaptureSource> &capture_sources) { - for(const CaptureSource &capture_source : capture_sources) { - if(capture_source.type == GSR_CAPTURE_SOURCE_TYPE_REGION && capture_source.region_set) - return true; - } - return false; -} - -static bool is_capturing_monitor_or_region(const std::vector<CaptureSource> &capture_sources) { - for(const CaptureSource &capture_source : capture_sources) { - if(capture_source.type == GSR_CAPTURE_SOURCE_TYPE_MONITOR || capture_source.type == GSR_CAPTURE_SOURCE_TYPE_REGION) - return true; - } - return false; -} - -static void validate_args_with_capture_sources(args_parser &arg_parser, const std::vector<CaptureSource> &capture_sources) { - const Arg *output_resolution_arg = args_parser_get_arg(&arg_parser, "-s"); - assert(output_resolution_arg); - - const Arg *region_arg = args_parser_get_arg(&arg_parser, "-region"); - assert(region_arg); - - if(is_capturing_type(capture_sources, GSR_CAPTURE_SOURCE_TYPE_FOCUSED_WINDOW) && output_resolution_arg->num_values == 0) { - fprintf(stderr, "gsr error: option -s is required when using '-w focused' option\n"); - args_parser_print_usage(); - _exit(1); - } - - const bool is_capturing_region = is_capturing_type(capture_sources, GSR_CAPTURE_SOURCE_TYPE_REGION); - if(region_arg->num_values == 0) { - if(is_capturing_region && !has_capture_source_with_region_set(capture_sources)) { - fprintf(stderr, "gsr error: option -region is required when '-w region' is used\n"); - args_parser_print_usage(); - _exit(1); - } - } else { - if(is_capturing_region) { - fprintf(stderr, "gsr warning: option -region is deprecated, use -w with region directly instead, for example: -w %s\n", region_arg->values[0]); - } else { - fprintf(stderr, "gsr error: option -region can only be used when option '-w region' is used\n"); - args_parser_print_usage(); - _exit(1); - } - } - - if(!arg_parser.restore_portal_session && is_capturing_type(capture_sources, GSR_CAPTURE_SOURCE_TYPE_PORTAL)) - fprintf(stderr, "gsr info: option '-w portal' was used without '-restore-portal-session yes'. The previous screencast session will be ignored\n"); -} - -static void install_cuda_no_stable_perf_limit() { - if(access("/proc/driver/nvidia/version", F_OK) != 0) - return; - - const char *home = getenv("HOME"); - if(!home) { - fprintf(stderr, "gsr warning: install_cuda_no_stable_perf_limit: $HOME not set\n"); - return; - } - - char nv_profiles_path[4096]; - snprintf(nv_profiles_path, sizeof(nv_profiles_path), "%s/.nv/nvidia-application-profiles-rc.d", home); - - if(create_directory_recursive(nv_profiles_path) != 0) { - fprintf(stderr, "gsr warning: install_cuda_no_stable_perf_limit: failed to create directory: %s\n", nv_profiles_path); - return; - } - - snprintf(nv_profiles_path, sizeof(nv_profiles_path), "%s/.nv/nvidia-application-profiles-rc.d/10-gsr-cuda-no-stable-perf-limit", home); - - FILE *f = fopen(nv_profiles_path, "wb"); - if(!f) { - fprintf(stderr, "gsr warning: install_cuda_no_stable_perf_limit: failed to create file: %s\n", nv_profiles_path); - return; - } - - const char *profile_data = - "{\n" - " \"profiles\": [\n" - " {\n" - " \"name\": \"CudaNoStablePerfLimit\",\n" - " \"settings\": [\"0x166c5e\", 0]\n" - " }\n" - " ],\n" - " \"rules\": [\n" - " { \"pattern\": \"gpu-screen-recorder\", \"profile\": \"CudaNoStablePerfLimit\" }\n" - " ]\n" - "}\n"; - - fwrite(profile_data, 1, strlen(profile_data), f); - fclose(f); -} - -int main(int argc, char **argv) { - setlocale(LC_ALL, "C"); // Sigh... stupid C -#ifdef __GLIBC__ - mallopt(M_MMAP_THRESHOLD, 65536); -#endif - - signal(SIGINT, stop_handler); - signal(SIGTERM, stop_handler); - signal(SIGUSR1, save_replay_handler); - signal(SIGUSR2, toggle_pause_handler); - signal(SIGRTMIN, toggle_replay_recording_handler); - signal(SIGRTMIN+1, save_replay_10_seconds_handler); - signal(SIGRTMIN+2, save_replay_30_seconds_handler); - signal(SIGRTMIN+3, save_replay_1_minute_handler); - signal(SIGRTMIN+4, save_replay_5_minutes_handler); - signal(SIGRTMIN+5, save_replay_10_minutes_handler); - signal(SIGRTMIN+6, save_replay_30_minutes_handler); - - set_display_server_environment_variables(); - install_cuda_no_stable_perf_limit(); - - // Linux nvidia driver 580.105.08 added the environment variable CUDA_DISABLE_PERF_BOOST to disable the p2 power level issue, - // where running cuda (which includes nvenc) causes the gpu to be forcefully set to p2 power level which on many nvidia gpus - // decreases gpu performance in games. On my GTX 1080 it decreased game performance by 10% for absolutely no reason. - setenv("CUDA_DISABLE_PERF_BOOST", "1", true); - // Stop nvidia driver from buffering frames - setenv("__GL_MaxFramesAllowed", "1", true); - // If this is set to 1 then cuGraphicsGLRegisterImage will fail for egl context with error: invalid OpenGL or DirectX context, - // so we overwrite it - setenv("__GL_THREADED_OPTIMIZATIONS", "0", true); - // Some people set this to nvidia (for nvdec) or vdpau (for nvidia vdpau), which breaks gpu screen recorder since - // nvidia doesn't support vaapi and nvidia-vaapi-driver doesn't support encoding yet. - // Let vaapi find the right vaapi driver instead of forcing a specific one. - unsetenv("LIBVA_DRIVER_NAME"); - // Some people set this to force all applications to vsync on nvidia, but this makes eglSwapBuffers never return. - unsetenv("__GL_SYNC_TO_VBLANK"); - // Same as above, but for amd/intel - unsetenv("vblank_mode"); - - if(geteuid() == 0) { - fprintf(stderr, "gsr error: don't run gpu-screen-recorder as the root user\n"); - _exit(1); - } - - args_handlers arg_handlers; - arg_handlers.version = version_command; - arg_handlers.info = info_command; - arg_handlers.list_audio_devices = list_audio_devices_command; - arg_handlers.list_application_audio = list_application_audio_command; - arg_handlers.list_v4l2_devices = list_v4l2_devices; - arg_handlers.list_capture_options = list_capture_options_command; - arg_handlers.list_monitors = list_monitors_command; - - args_parser arg_parser; - if(!args_parser_parse(&arg_parser, argc, argv, &arg_handlers, NULL)) - _exit(1); - - if(!arg_parser.low_power) { - // Forces low latency encoding mode. Use this environment variable until vaapi supports setting this as a parameter. - // The downside of this is that it always uses maximum power, which is not ideal for replay mode that runs on system startup. - // This option was added in mesa 24.1.4, released in july 17, 2024. - // Seems like the performance issue is not in encoding, but rendering the frame. - // Some frames end up taking 10 times longer. Seems to be an issue with amd gpu power management when letting the application sleep on the cpu side? - setenv("AMD_DEBUG", "lowlatencyenc", true); - } - - std::vector<CaptureSource> capture_sources = parse_capture_source_arg(arg_parser.capture_source, arg_parser); - if(capture_sources.empty()) { - fprintf(stderr, "gsr error: option -w can't be empty. You need to capture video from at least one source\n"); - args_parser_print_usage(); - _exit(1); - } - validate_args_with_capture_sources(arg_parser, capture_sources); - - //av_log_set_level(AV_LOG_TRACE); - - const Arg *audio_input_arg = args_parser_get_arg(&arg_parser, "-a"); - assert(audio_input_arg); - - AudioDevices audio_devices; - if(audio_input_arg->num_values > 0) - audio_devices = get_pulseaudio_inputs(); - - std::vector<MergedAudioInputs> requested_audio_inputs = parse_audio_inputs(audio_devices, audio_input_arg); - - const bool uses_app_audio = merged_audio_inputs_has_app_audio(requested_audio_inputs); - std::vector<std::string> app_audio_names; -#ifdef GSR_APP_AUDIO - gsr_pipewire_audio pipewire_audio; - memset(&pipewire_audio, 0, sizeof(pipewire_audio)); - if(uses_app_audio) { - if(!pulseaudio_server_is_pipewire()) { - fprintf(stderr, "gsr error: your sound server is not PipeWire. Application audio is only available when running PipeWire audio server\n"); - _exit(2); - } - - if(!gsr_pipewire_audio_init(&pipewire_audio)) { - fprintf(stderr, "gsr error: failed to setup PipeWire audio for application audio capture\n"); - _exit(2); - } - - gsr_pipewire_audio_for_each_app(&pipewire_audio, [](const char *app_name, void *userdata) { - std::vector<std::string> *app_audio_names = (std::vector<std::string>*)userdata; - app_audio_names->push_back(app_name); - return true; - }, &app_audio_names); - } -#else - if(uses_app_audio) { - fprintf(stderr, "gsr error: application audio can't be recorded because GPU Screen Recorder is built without application audio support (-Dapp_audio option)\n"); - _exit(2); - } -#endif - - validate_merged_audio_inputs_app_audio(requested_audio_inputs, app_audio_names); - - bool wayland = false; - Display *dpy = XOpenDisplay(nullptr); - if(dpy) { - XSelectInput(dpy, DefaultRootWindow(dpy), PropertyChangeMask); - } else { - wayland = true; - fprintf(stderr, "gsr warning: failed to connect to the X server. Assuming wayland is running without Xwayland\n"); - } - - XSetErrorHandler(x11_error_handler); - XSetIOErrorHandler(x11_io_error_handler); - - if(!wayland) - wayland = is_xwayland(dpy); - - if(!wayland && is_using_prime_run()) { - // Disable prime-run and similar options as it doesn't work, the monitor to capture has to be run on the same device. - // This is fine on wayland since nvidia uses drm interface there and the monitor query checks the monitors connected - // to the drm device. - fprintf(stderr, "gsr warning: use of prime-run on X11 is not supported. Disabling prime-run\n"); - disable_prime_run(); - } - - gsr_window *window = gsr_window_create(dpy, wayland); - if(!window) { - fprintf(stderr, "gsr error: failed to create window\n"); - _exit(1); - } - - if(is_capturing_type(capture_sources, GSR_CAPTURE_SOURCE_TYPE_PORTAL)) { - if(is_using_prime_run()) { - fprintf(stderr, "gsr warning: use of prime-run with -w portal option is currently not supported. Disabling prime-run\n"); - disable_prime_run(); - } - - if(video_codec_is_hdr(arg_parser.video_codec)) { - fprintf(stderr, "gsr warning: portal capture option doesn't support hdr yet (PipeWire doesn't support hdr), the video will be tonemapped from hdr to sdr\n"); - arg_parser.video_codec = hdr_video_codec_to_sdr_video_codec(arg_parser.video_codec); - } - } - - gsr_egl egl; - if(!gsr_egl_load(&egl, window, is_capturing_monitor_or_region(capture_sources), arg_parser.gl_debug)) { - fprintf(stderr, "gsr error: failed to load opengl\n"); - _exit(1); - } - - gsr_shader_enable_debug_output(arg_parser.gl_debug); -#ifndef NDEBUG - gsr_shader_enable_debug_output(true); -#endif - - if(!args_parser_validate_with_gl_info(&arg_parser, &egl)) - _exit(1); - - egl.card_path[0] = '\0'; - if(monitor_capture_use_drm(window, egl.gpu_info.vendor)) { - // TODO: Allow specifying another card, and in other places - if(!gsr_get_valid_card_path(&egl, egl.card_path, is_capturing_monitor_or_region(capture_sources))) { - fprintf(stderr, "gsr error: no /dev/dri/cardX device found. Make sure that you have at least one monitor connected or record a single window instead on X11 or record with the -w portal option\n"); - _exit(2); - } - } else { - gsr_get_valid_card_path(&egl, egl.card_path, false); - } - - memset(&x11_cursor, 0, sizeof(x11_cursor)); - x11_cursor_display = NULL; - if(gsr_window_get_display_server(window) == GSR_DISPLAY_SERVER_X11 && arg_parser.record_cursor) { - x11_cursor_display = (Display*)gsr_window_get_display(egl.window); - gsr_cursor_init(&x11_cursor, &egl, x11_cursor_display); - } - - // if(wayland && arg_parser.capture_source_type == GSR_CAPTURE_SOURCE_TYPE_MONITOR) { - // fprintf(stderr, "gsr warning: it's not possible to sync video to recorded monitor exactly on wayland when recording a monitor." - // " If you experience stutter in the video then record with portal capture option instead (-w portal) or use X11 instead\n"); - // } - - gsr_image_format image_format; - if(get_image_format_from_filename(arg_parser.filename, &image_format)) { - if(audio_input_arg->num_values > 0) { - fprintf(stderr, "gsr error: can't record audio (-a) when taking a screenshot\n"); - _exit(1); - } - - capture_image_to_file(arg_parser, &egl, window, image_format, capture_sources); - _exit(0); - } - - AVFormatContext *av_format_context; - // The output format is automatically guessed by the file extension - avformat_alloc_output_context2(&av_format_context, nullptr, arg_parser.container_format, arg_parser.filename); - if (!av_format_context) { - if(arg_parser.container_format) { - fprintf(stderr, "gsr error: Container format '%s' (argument -c) is not valid\n", arg_parser.container_format); - } else { - fprintf(stderr, "gsr error: Failed to deduce container format from file extension. Use the '-c' option to specify container format\n"); - args_parser_print_usage(); - _exit(1); - } - _exit(1); - } - - set_format_context_options(av_format_context); - - const AVOutputFormat *output_format = av_format_context->oformat; - - std::string file_extension = output_format->extensions ? output_format->extensions : ""; - { - size_t comma_index = file_extension.find(','); - if(comma_index != std::string::npos) - file_extension = file_extension.substr(0, comma_index); - } - - if(file_extension.empty()) - file_extension = arg_parser.container_format ? arg_parser.container_format : ""; - - const bool force_no_audio_offset = arg_parser.is_livestream || arg_parser.is_output_piped || (file_extension != "mp4" && file_extension != "mkv" && file_extension != "webm"); - const double target_fps = 1.0 / (double)arg_parser.fps; - - const bool uses_amix = merged_audio_inputs_should_use_amix(requested_audio_inputs); - arg_parser.audio_codec = select_audio_codec_with_fallback(arg_parser.audio_codec, file_extension, uses_amix); - - vec2i video_size = {0, 0}; - std::vector<VideoSource> video_sources = create_video_sources(arg_parser, &egl, false, capture_sources, video_size); - - // (Some?) livestreaming services require at least one audio track to work. - // If not audio is provided then create one silent audio track. - if(arg_parser.is_livestream && requested_audio_inputs.empty()) { - fprintf(stderr, "gsr info: live streaming but no audio track was added. Adding a silent audio track\n"); - MergedAudioInputs mai; - mai.audio_inputs.push_back({""}); - requested_audio_inputs.push_back(std::move(mai)); - } - - AVStream *video_stream = nullptr; - std::vector<AudioTrack> audio_tracks; - - if(arg_parser.video_encoder == GSR_VIDEO_ENCODER_HW_CPU && arg_parser.video_codec != (gsr_video_codec)GSR_VIDEO_CODEC_AUTO && arg_parser.video_codec != GSR_VIDEO_CODEC_H264) { - fprintf(stderr, "gsr error: -encoder cpu was specified but a codec other than h264 was specified. -encoder cpu supports only h264 at the moment\n"); - _exit(1); - } - - bool low_power = false; - const AVCodec *video_codec_f = select_video_codec_with_fallback(video_size, &arg_parser, file_extension.c_str(), &egl, &low_power); - - const enum AVPixelFormat video_pix_fmt = get_pixel_format(arg_parser.video_codec, egl.gpu_info.vendor, arg_parser.video_encoder == GSR_VIDEO_ENCODER_HW_CPU); - AVCodecContext *video_codec_context = create_video_codec_context(video_pix_fmt, video_codec_f, egl, arg_parser, video_size.x, video_size.y); - if(!arg_parser.is_replaying) - video_stream = create_stream(av_format_context, video_codec_context); - - AVFrame *video_frame = av_frame_alloc(); - if(!video_frame) { - fprintf(stderr, "gsr error: Failed to allocate video frame\n"); - _exit(1); - } - video_frame->format = video_codec_context->pix_fmt; - video_frame->width = video_size.x; - video_frame->height = video_size.y; - video_frame->color_range = video_codec_context->color_range; - video_frame->color_primaries = video_codec_context->color_primaries; - video_frame->color_trc = video_codec_context->color_trc; - video_frame->colorspace = video_codec_context->colorspace; - video_frame->chroma_location = video_codec_context->chroma_sample_location; - - const size_t estimated_replay_buffer_packets = calculate_estimated_replay_buffer_packets(arg_parser.replay_buffer_size_secs, arg_parser.fps, arg_parser.audio_codec, requested_audio_inputs); - gsr_encoder encoder; - if(!gsr_encoder_init(&encoder, arg_parser.replay_storage, estimated_replay_buffer_packets, arg_parser.replay_buffer_size_secs, arg_parser.filename)) { - fprintf(stderr, "gsr error: failed to create encoder\n"); - _exit(1); - } - - gsr_video_encoder *video_encoder = create_video_encoder(&egl, arg_parser); - if(!video_encoder) { - fprintf(stderr, "gsr error: failed to create video encoder\n"); - _exit(1); - } - - if(!gsr_video_encoder_start(video_encoder, video_codec_context, video_frame)) { - fprintf(stderr, "gsr error: failed to start video encoder\n"); - _exit(1); - } - - video_size.x = video_codec_context->width; - video_size.y = video_codec_context->height; - video_sources_update_with_real_video_size(capture_sources, video_sources, video_size); - - const Arg *plugin_arg = args_parser_get_arg(&arg_parser, "-p"); - assert(plugin_arg); - - gsr_plugins plugins; - memset(&plugins, 0, sizeof(plugins)); - - if(plugin_arg->num_values > 0) - load_plugins(&plugins, arg_parser, &egl, video_size); - - gsr_color_conversion_params color_conversion_params; - memset(&color_conversion_params, 0, sizeof(color_conversion_params)); - color_conversion_params.color_range = arg_parser.color_range; - color_conversion_params.egl = &egl; - color_conversion_params.load_external_image_shader = any_video_sources_uses_external_image(video_sources); - gsr_video_encoder_get_textures(video_encoder, color_conversion_params.destination_textures, color_conversion_params.destination_textures_size, &color_conversion_params.num_destination_textures, &color_conversion_params.destination_color); - - gsr_color_conversion color_conversion; - if(gsr_color_conversion_init(&color_conversion, &color_conversion_params) != 0) { - fprintf(stderr, "gsr error: main: failed to create color conversion\n"); - _exit(1); - } - - gsr_color_conversion_clear(&color_conversion); - - gsr_color_conversion *output_color_conversion = plugins.num_plugins > 0 ? &plugins.color_conversion : &color_conversion; - - if(arg_parser.video_encoder == GSR_VIDEO_ENCODER_HW_CPU) { - open_video_software(video_codec_context, arg_parser); - } else { - open_video_hardware(video_codec_context, low_power, egl, arg_parser); - } - - if(video_stream) { - avcodec_parameters_from_context(video_stream->codecpar, video_codec_context); - const size_t video_destination_id = gsr_encoder_add_recording_destination(&encoder, video_codec_context, av_format_context, video_stream, 0); - if(arg_parser.write_first_frame_ts && video_destination_id != (size_t)-1) { - std::string ts_filepath = std::string(arg_parser.filename) + ".ts"; - gsr_encoder_set_recording_destination_first_frame_ts_filepath(&encoder, video_destination_id, ts_filepath.c_str()); - } - } - - int audio_max_frame_size = 1024; - int audio_stream_index = VIDEO_STREAM_INDEX + 1; - for(const MergedAudioInputs &merged_audio_inputs : requested_audio_inputs) { - const bool use_amix = audio_inputs_should_use_amix(merged_audio_inputs.audio_inputs); - AVCodecContext *audio_codec_context = create_audio_codec_context(arg_parser.fps, arg_parser.audio_codec, use_amix, arg_parser.audio_bitrate); - - AVStream *audio_stream = nullptr; - if(!arg_parser.is_replaying) { - audio_stream = create_stream(av_format_context, audio_codec_context); - if(gsr_encoder_add_recording_destination(&encoder, audio_codec_context, av_format_context, audio_stream, 0) == (size_t)-1) - fprintf(stderr, "gsr error: added too many audio sources\n"); - } - - if(audio_stream && !merged_audio_inputs.track_name.empty() && !arg_parser.exclude_metadata) - av_dict_set(&audio_stream->metadata, "title", merged_audio_inputs.track_name.c_str(), 0); - - open_audio(audio_codec_context, arg_parser.ffmpeg_audio_opts); - if(audio_stream) - avcodec_parameters_from_context(audio_stream->codecpar, audio_codec_context); - - #if LIBAVCODEC_VERSION_MAJOR < 60 - const int num_channels = audio_codec_context->channels; - #else - const int num_channels = audio_codec_context->ch_layout.nb_channels; - #endif - - //audio_frame->sample_rate = audio_codec_context->sample_rate; - - std::vector<AVFilterContext*> src_filter_ctx; - AVFilterGraph *graph = nullptr; - AVFilterContext *sink = nullptr; - if(use_amix) { - int err = init_filter_graph(audio_codec_context, &graph, &sink, src_filter_ctx, merged_audio_inputs.audio_inputs.size()); - if(err < 0) { - fprintf(stderr, "gsr error: failed to create audio filter\n"); - _exit(1); - } - } - - // TODO: Cleanup above - - const double audio_fps = (double)audio_codec_context->sample_rate / (double)audio_codec_context->frame_size; - const double timeout_sec = 1000.0 / audio_fps / 1000.0; - - const double audio_startup_time_seconds = force_no_audio_offset ? 0 : audio_codec_get_desired_delay(arg_parser.audio_codec, arg_parser.fps);// * ((double)audio_codec_context->frame_size / 1024.0); - const double num_audio_frames_shift = audio_startup_time_seconds / timeout_sec; - - std::vector<AudioDeviceData> audio_track_audio_devices; - if(audio_inputs_has_app_audio(merged_audio_inputs.audio_inputs)) { - assert(!use_amix); -#ifdef GSR_APP_AUDIO - audio_track_audio_devices.push_back(create_application_audio_audio_input(merged_audio_inputs, audio_codec_context, num_channels, num_audio_frames_shift, &pipewire_audio)); -#endif - } else { - audio_track_audio_devices = create_device_audio_inputs(merged_audio_inputs.audio_inputs, audio_codec_context, num_channels, num_audio_frames_shift, src_filter_ctx, use_amix); - } - - AudioTrack audio_track; - audio_track.name = merged_audio_inputs.track_name; - audio_track.codec_context = audio_codec_context; - audio_track.audio_devices = std::move(audio_track_audio_devices); - audio_track.graph = graph; - audio_track.sink = sink; - audio_track.stream_index = audio_stream_index; - audio_track.pts = -audio_codec_context->frame_size * num_audio_frames_shift; - audio_tracks.push_back(std::move(audio_track)); - ++audio_stream_index; - - audio_max_frame_size = std::max(audio_max_frame_size, audio_codec_context->frame_size); - } - - //av_dump_format(av_format_context, 0, filename, 1); - - if(!arg_parser.is_replaying && !(output_format->flags & AVFMT_NOFILE)) { - const int ret = avio_open(&av_format_context->pb, arg_parser.filename, AVIO_FLAG_WRITE); - if(ret < 0) { - fprintf(stderr, "gsr error: Could not open '%s': %s\n", arg_parser.filename, av_error_to_string(ret)); - _exit(1); - } - } - - if(!arg_parser.is_replaying) - av_write_header(av_format_context, arg_parser.ffmpeg_opts); - - double fps_start_time = clock_get_monotonic_seconds(); - //double frame_timer_start = fps_start_time; - int fps_counter = 0; - int damage_fps_counter = 0; - - bool paused = false; - std::atomic<double> paused_time_offset(0.0); - double paused_time_start = 0.0; - bool replay_recording = false; - RecordingStartResult replay_recording_start_result; - std::vector<size_t> replay_recording_items; - std::string replay_recording_filepath; - bool force_iframe_frame = false; // Only needed for video since audio frames are always iframes - - std::mutex audio_filter_mutex; - - const double record_start_time = clock_get_monotonic_seconds(); - - const size_t audio_buffer_size = audio_max_frame_size * 4 * 2; // max 4 bytes/sample, 2 channels - uint8_t *empty_audio = (uint8_t*)malloc(audio_buffer_size); - if(!empty_audio) { - fprintf(stderr, "gsr error: failed to create empty audio\n"); - _exit(1); - } - memset(empty_audio, 0, audio_buffer_size); - - for(AudioTrack &audio_track : audio_tracks) { - for(AudioDeviceData &audio_device : audio_track.audio_devices) { - audio_device.thread = std::thread([&]() mutable { - const AVSampleFormat sound_device_sample_format = audio_format_to_sample_format(audio_codec_context_get_audio_format(audio_track.codec_context)); - // TODO: Always do conversion for now. This fixes issue with stuttering audio on pulseaudio with opus + multiple audio sources merged - const bool needs_audio_conversion = true;//audio_track.codec_context->sample_fmt != sound_device_sample_format; - SwrContext *swr = nullptr; - if(needs_audio_conversion) { - swr = swr_alloc(); - if(!swr) { - fprintf(stderr, "Failed to create SwrContext\n"); - _exit(1); - } - #if LIBAVUTIL_VERSION_MAJOR <= 56 - av_opt_set_channel_layout(swr, "in_channel_layout", AV_CH_LAYOUT_STEREO, 0); - av_opt_set_channel_layout(swr, "out_channel_layout", AV_CH_LAYOUT_STEREO, 0); - #elif LIBAVUTIL_VERSION_MAJOR >= 59 - av_opt_set_chlayout(swr, "in_chlayout", &audio_track.codec_context->ch_layout, 0); - av_opt_set_chlayout(swr, "out_chlayout", &audio_track.codec_context->ch_layout, 0); - #else - av_opt_set_chlayout(swr, "in_channel_layout", &audio_track.codec_context->ch_layout, 0); - av_opt_set_chlayout(swr, "out_channel_layout", &audio_track.codec_context->ch_layout, 0); - #endif - av_opt_set_int(swr, "in_sample_rate", audio_track.codec_context->sample_rate, 0); - av_opt_set_int(swr, "out_sample_rate", audio_track.codec_context->sample_rate, 0); - av_opt_set_sample_fmt(swr, "in_sample_fmt", sound_device_sample_format, 0); - av_opt_set_sample_fmt(swr, "out_sample_fmt", audio_track.codec_context->sample_fmt, 0); - swr_init(swr); - } - - const double audio_fps = (double)audio_track.codec_context->sample_rate / (double)audio_track.codec_context->frame_size; - const int64_t timeout_ms = std::round(1000.0 / audio_fps); - const double timeout_sec = 1000.0 / audio_fps / 1000.0; - int64_t num_received_frames = 0; - - // The sound device is opened before the recording starts, so it can contain old audio from before the recording started. - // Discard it so the recording doesn't start with old audio. - if(audio_device.sound_device.handle) - sound_device_flush(&audio_device.sound_device); - - while(running) { - void *sound_buffer; - int sound_buffer_size = -1; - const double time_before_read_seconds = clock_get_monotonic_seconds(); - if(audio_device.sound_device.handle) { - // TODO: use this instead of calculating time to read. But this can fluctuate and we dont want to go back in time, - // also it's 0.0 for some users??? - double latency_seconds = 0.0; - sound_buffer_size = sound_device_read_next_chunk(&audio_device.sound_device, &sound_buffer, timeout_sec * 2.0, &latency_seconds); - } - - const bool got_audio_data = sound_buffer_size >= 0; - //fprintf(stderr, "got audio data: %s\n", got_audio_data ? "yes" : "no"); - //fprintf(stderr, "time to read: %f, %s, %f\n", time_to_read_seconds, got_audio_data ? "yes" : "no", timeout_sec); - const double this_audio_frame_time = clock_get_monotonic_seconds() - paused_time_offset; - - if(paused) { - if(!audio_device.sound_device.handle) - av_usleep(timeout_ms * 1000); - - continue; - } - - int ret = av_frame_make_writable(audio_device.frame); - if (ret < 0) { - fprintf(stderr, "Failed to make audio frame writable\n"); - break; - } - - // TODO: Is this |received_audio_time| really correct? - const int64_t num_expected_frames = std::floor((this_audio_frame_time - record_start_time) / timeout_sec); - int64_t num_missing_frames = std::max((int64_t)0LL, num_expected_frames - num_received_frames); - - if(got_audio_data) - num_missing_frames = std::max((int64_t)0LL, num_missing_frames - 1); - - if(!audio_device.sound_device.handle) - num_missing_frames = std::max((int64_t)1, num_missing_frames); - - // Fucking hell is there a better way to do this? I JUST WANT TO KEEP VIDEO AND AUDIO SYNCED HOLY FUCK I WANT TO KILL MYSELF NOW. - // THIS PIECE OF SHIT WANTS EMPTY FRAMES OTHERWISE VIDEO PLAYS TOO FAST TO KEEP UP WITH AUDIO OR THE AUDIO PLAYS TOO EARLY. - // BUT WE CANT USE DELAYS TO GIVE DUMMY DATA BECAUSE PULSEAUDIO MIGHT GIVE AUDIO A BIG DELAYED!!! - // This garbage is needed because we want to produce constant frame rate videos instead of variable frame rate - // videos because bad software such as video editing software and VLC do not support variable frame rate software, - // despite nvidia shadowplay and xbox game bar producing variable frame rate videos. - // So we have to make sure we produce frames at the same relative rate as the video. - if((num_missing_frames >= 1 && got_audio_data) || num_missing_frames >= 5 || !audio_device.sound_device.handle) { - // Fill the missing frames with silence. Duplicating the previous audio frame to fill the gap instead - // sounds like a stutter and it's especially noticeable at the start of the recording when the audio device - // hasn't started to deliver audio at a stable rate yet, which repeats the first audio frame multiple times. - if(needs_audio_conversion) - swr_convert(swr, &audio_device.frame->data[0], audio_track.codec_context->frame_size, (const uint8_t**)&empty_audio, audio_track.codec_context->frame_size); - else - audio_device.frame->data[0] = empty_audio; - - // TODO: Check if duplicate frame can be saved just by writing it with a different pts instead of sending it again - std::lock_guard<std::mutex> lock(audio_filter_mutex); - for(int i = 0; i < num_missing_frames; ++i) { - if(audio_track.graph) { - // TODO: av_buffersrc_add_frame - if(av_buffersrc_write_frame(audio_device.src_filter_ctx, audio_device.frame) < 0) { - fprintf(stderr, "gsr error: failed to add audio frame to filter\n"); - } - } else { - ret = avcodec_send_frame(audio_track.codec_context, audio_device.frame); - if(ret >= 0) { - // TODO: Move to separate thread because this could write to network (for example when livestreaming) - gsr_encoder_receive_packets(&encoder, audio_track.codec_context, audio_device.frame->pts, audio_track.stream_index); - } else { - fprintf(stderr, "Failed to encode audio!\n"); - } - audio_track.pts += audio_track.codec_context->frame_size; - } - - audio_device.frame->pts += audio_track.codec_context->frame_size; - num_received_frames++; - } - } - - if(!audio_device.sound_device.handle) { - av_usleep(timeout_ms * 1000); - } else if(got_audio_data) { - // The frame has to be made writable again if the frame was already sent to the audio filter above (when filling missing frames) - // because the audio filter only references the frame data instead of copying it. Without this the sent frames data would be - // overwritten with the audio data below, causing the audio to repeat instead of the missing frames being silent. - ret = av_frame_make_writable(audio_device.frame); - if (ret < 0) { - fprintf(stderr, "Failed to make audio frame writable\n"); - break; - } - - // TODO: Instead of converting audio, get float audio from alsa. Or does alsa do conversion internally to get this format? - if(needs_audio_conversion) - swr_convert(swr, &audio_device.frame->data[0], audio_track.codec_context->frame_size, (const uint8_t**)&sound_buffer, audio_track.codec_context->frame_size); - else - audio_device.frame->data[0] = (uint8_t*)sound_buffer; - - std::lock_guard<std::mutex> lock(audio_filter_mutex); - - if(audio_track.graph) { - // TODO: av_buffersrc_add_frame - if(av_buffersrc_write_frame(audio_device.src_filter_ctx, audio_device.frame) < 0) { - fprintf(stderr, "gsr error: failed to add audio frame to filter\n"); - } - } else { - ret = avcodec_send_frame(audio_track.codec_context, audio_device.frame); - if(ret >= 0) { - // TODO: Move to separate thread because this could write to network (for example when livestreaming) - gsr_encoder_receive_packets(&encoder, audio_track.codec_context, audio_device.frame->pts, audio_track.stream_index); - } else { - fprintf(stderr, "Failed to encode audio!\n"); - } - audio_track.pts += audio_track.codec_context->frame_size; - } - - audio_device.frame->pts += audio_track.codec_context->frame_size; - num_received_frames++; - } else { - // TODO: Maybe sleep for time_to_sleep_until_next_frame/4? for better latency - const double time_after_read_seconds = clock_get_monotonic_seconds(); - const double time_to_read_seconds = time_after_read_seconds - time_before_read_seconds; - const double time_to_sleep_until_next_frame = timeout_sec - time_to_read_seconds; - if(time_to_sleep_until_next_frame > 0.0) - av_usleep(time_to_sleep_until_next_frame * 1000ULL * 1000ULL); - } - } - - if(swr) - swr_free(&swr); - }); - } - } - - std::thread amix_thread; - if(uses_amix) { - amix_thread = std::thread([&]() { - AVFrame *aframe = av_frame_alloc(); - while(running) { - { - std::lock_guard<std::mutex> lock(audio_filter_mutex); - for(AudioTrack &audio_track : audio_tracks) { - if(!audio_track.sink) - continue; - - int err = 0; - while ((err = av_buffersink_get_frame(audio_track.sink, aframe)) >= 0) { - aframe->pts = audio_track.pts; - err = avcodec_send_frame(audio_track.codec_context, aframe); - if(err >= 0){ - // TODO: Move to separate thread because this could write to network (for example when livestreaming) - gsr_encoder_receive_packets(&encoder, audio_track.codec_context, aframe->pts, audio_track.stream_index); - } else { - fprintf(stderr, "Failed to encode audio!\n"); - } - av_frame_unref(aframe); - audio_track.pts += audio_track.codec_context->frame_size; - } - } - } - av_usleep(5 * 1000); // 5 milliseconds - } - av_frame_free(&aframe); - }); - } - - // Set update_fps to 24 to test if duplicate/delayed frames cause video/audio desync or too fast/slow video. - //const double update_fps = fps + 190; - bool should_stop_error = false; - - int64_t video_pts_counter = 0; - int64_t video_prev_pts = 0; - - bool hdr_metadata_set = false; - const bool hdr = video_codec_is_hdr(arg_parser.video_codec); - - bool use_damage_tracking = false; - gsr_damage damage; - memset(&damage, 0, sizeof(damage)); - if(arg_parser.framerate_mode == GSR_FRAMERATE_MODE_CONTENT && is_capturing_damage_tracked_target(capture_sources)) { - if(gsr_window_get_display_server(window) == GSR_DISPLAY_SERVER_X11) { - gsr_damage_init(&damage, &egl, &x11_cursor, arg_parser.record_cursor); - use_damage_tracking = true; - - for(const CaptureSource &capture_source : capture_sources) { - switch(capture_source.type) { - case GSR_CAPTURE_SOURCE_TYPE_WINDOW: - gsr_damage_start_tracking_window(&damage, capture_source.window_id); - break; - case GSR_CAPTURE_SOURCE_TYPE_MONITOR: - case GSR_CAPTURE_SOURCE_TYPE_REGION: - // TODO: When capturing a region only track damage in that region - gsr_damage_start_tracking_monitor(&damage, capture_source.name.c_str()); - break; - default: - break; - } - } - } else if(is_capturing_monitor_or_region(capture_sources)) { - fprintf(stderr, "gsr warning: \"-fm content\" has no effect on Wayland when recording a monitor. Either record a monitor on X11 or capture with desktop portal instead (-w portal)\n"); - } - } - - while(running) { - while(gsr_window_process_event(window)) { - if(x11_cursor_display && arg_parser.record_cursor) - gsr_cursor_on_event(&x11_cursor, gsr_window_get_event_data(window)); - - gsr_damage_on_event(&damage, gsr_window_get_event_data(window)); - for(VideoSource &video_source : video_sources) { - gsr_capture_on_event(video_source.capture, &egl); - } - } - - if(x11_cursor_display && arg_parser.record_cursor) - gsr_cursor_tick(&x11_cursor, DefaultRootWindow(x11_cursor_display)); - - gsr_damage_tick(&damage); - - should_stop_error = false; - bool damaged = false; - - if(use_damage_tracking) - damaged = gsr_damage_is_damaged(&damage); - - for(VideoSource &video_source : video_sources) { - gsr_capture_tick(video_source.capture); - - if(gsr_capture_should_stop(video_source.capture, &should_stop_error)) { - running = 0; - break; - } - - if(video_source.capture_source->type == GSR_CAPTURE_SOURCE_TYPE_FOCUSED_WINDOW) { - assert(video_source.capture->get_window_id); - const Window damage_target_window = video_source.capture->get_window_id(video_source.capture); - - if((int64_t)damage_target_window != video_source.capture_source->window_id) { - gsr_damage_stop_tracking_window(&damage, video_source.capture_source->window_id); - if(damage_target_window != 0) - gsr_damage_start_tracking_window(&damage, damage_target_window); - } - - video_source.capture_source->window_id = damage_target_window; - } - - if(video_source.capture->is_damaged) - damaged |= video_source.capture->is_damaged(video_source.capture); - else if(!use_damage_tracking) - damaged = true; - } - - damaged |= gsr_plugins_is_damaged(&plugins); - - // TODO: Readd wayland sync warning when removing this - if(arg_parser.framerate_mode != GSR_FRAMERATE_MODE_CONTENT) - damaged = true; - - if(damaged) - ++damage_fps_counter; - - ++fps_counter; - const double time_now = clock_get_monotonic_seconds(); - //const double frame_timer_elapsed = time_now - frame_timer_start; - const double elapsed = time_now - fps_start_time; - if (elapsed >= 1.0) { - if(arg_parser.verbose) { - fprintf(stderr, "update fps: %d, damage fps: %d\n", fps_counter, damage_fps_counter); - } - fps_start_time = time_now; - fps_counter = 0; - damage_fps_counter = 0; - } - - const double this_video_frame_time = clock_get_monotonic_seconds() - paused_time_offset; - const int64_t expected_frames = std::floor((this_video_frame_time - record_start_time) / target_fps); - const int64_t num_missed_frames = expected_frames - video_pts_counter; - - if(damaged && num_missed_frames >= 1 && !paused) { - // TODO: Dont do this if no damage? - egl.glClear(0); - - gsr_damage_clear(&damage); - gsr_plugins_clear_damage(&plugins); - gsr_capture_kms_cleanup_kms_fds(); - - if(kms_client_initialized) { - if(gsr_kms_client_get_kms(&kms_client, &kms_response) != 0) - fprintf(stderr, "gsr error: failed to get kms, error: %d (%s)\n", kms_response.result, kms_response.err_msg); - } - - bool capture_has_synchronous_task = false; - for(VideoSource &video_source : video_sources) { - if(video_source.capture->clear_damage) - video_source.capture->clear_damage(video_source.capture); - - if(video_source.capture->capture_has_synchronous_task) { - capture_has_synchronous_task = video_source.capture->capture_has_synchronous_task(video_source.capture); - if(capture_has_synchronous_task) { - paused_time_start = clock_get_monotonic_seconds(); - paused = true; - } - } - } - - for(VideoSource &video_source : video_sources) { - if(video_source.capture->pre_capture) - video_source.capture->pre_capture(video_source.capture, &video_source.metadata, output_color_conversion); - } - - if(output_color_conversion->schedule_clear) { - output_color_conversion->schedule_clear = false; - gsr_color_conversion_clear(output_color_conversion); - } - - for(VideoSource &video_source : video_sources) { - gsr_capture_capture(video_source.capture, &video_source.metadata, output_color_conversion); - } - - gsr_capture_kms_cleanup_kms_fds(); - - if(plugins.num_plugins > 0) { - gsr_plugins_draw(&plugins); - gsr_color_conversion_draw(&color_conversion, plugins.texture, - {0, 0}, video_size, - {0, 0}, video_size, - video_size, GSR_ROT_0, GSR_FLIP_NONE, GSR_SOURCE_COLOR_RGB, false); - } - - if(capture_has_synchronous_task) { - paused_time_offset = paused_time_offset + (clock_get_monotonic_seconds() - paused_time_start); - paused = false; - } - - gsr_egl_swap_buffers(&egl); - gsr_video_encoder_copy_textures_to_frame(video_encoder, video_frame, output_color_conversion); - - for(VideoSource &video_source : video_sources) { - if(hdr && !hdr_metadata_set && !arg_parser.is_replaying && add_hdr_metadata_to_video_stream(video_source.capture, video_stream)) - hdr_metadata_set = true; - } - - // TODO: Check if duplicate frame can be saved just by writing it with a different pts instead of sending it again - const int num_frames_to_encode = arg_parser.framerate_mode == GSR_FRAMERATE_MODE_CONSTANT ? num_missed_frames : 1; - for(int i = 0; i < num_frames_to_encode; ++i) { - if(arg_parser.framerate_mode == GSR_FRAMERATE_MODE_CONSTANT) { - video_frame->pts = video_pts_counter + i; - } else { - video_frame->pts = (this_video_frame_time - record_start_time) * (double)AV_TIME_BASE; - const bool same_pts = video_frame->pts == video_prev_pts; - video_prev_pts = video_frame->pts; - if(same_pts) - continue; - } - - if(force_iframe_frame) { - video_frame->pict_type = AV_PICTURE_TYPE_I; - } - - int ret = avcodec_send_frame(video_codec_context, video_frame); - if(ret == 0) { - // TODO: Move to separate thread because this could write to network (for example when livestreaming) - gsr_encoder_receive_packets(&encoder, video_codec_context, video_frame->pts, VIDEO_STREAM_INDEX); - } else { - fprintf(stderr, "gsr error: avcodec_send_frame failed, error: %s\n", av_error_to_string(ret)); - } - - if(force_iframe_frame) { - force_iframe_frame = false; - video_frame->pict_type = AV_PICTURE_TYPE_NONE; - } - } - - video_pts_counter += num_missed_frames; - } - - if(toggle_pause == 1 && !arg_parser.is_replaying) { - const bool new_paused_state = !paused; - if(new_paused_state) { - paused_time_start = clock_get_monotonic_seconds(); - fprintf(stderr, "Paused\n"); - } else { - paused_time_offset = paused_time_offset + (clock_get_monotonic_seconds() - paused_time_start); - fprintf(stderr, "Unpaused\n"); - } - - toggle_pause = 0; - paused = !paused; - } - - if(toggle_replay_recording && !arg_parser.replay_recording_directory) { - toggle_replay_recording = 0; - printf("gsr error: Unable to start recording since the -ro option was not specified\n"); - fflush(stdout); - } - - if(toggle_replay_recording && arg_parser.replay_recording_directory) { - toggle_replay_recording = 0; - const bool new_replay_recording_state = !replay_recording; - if(new_replay_recording_state) { - std::lock_guard<std::mutex> lock(audio_filter_mutex); - replay_recording_items.clear(); - replay_recording_filepath = create_new_recording_filepath_from_timestamp(arg_parser.replay_recording_directory, "Video", file_extension, arg_parser.date_folders); - replay_recording_start_result = start_recording_create_streams(replay_recording_filepath.c_str(), arg_parser, video_codec_context, audio_tracks, hdr, video_sources); - if(replay_recording_start_result.av_format_context) { - const size_t video_recording_destination_id = gsr_encoder_add_recording_destination(&encoder, video_codec_context, replay_recording_start_result.av_format_context, replay_recording_start_result.video_stream, video_frame->pts); - if(arg_parser.write_first_frame_ts && video_recording_destination_id != (size_t)-1) { - std::string ts_filepath = replay_recording_filepath + ".ts"; - gsr_encoder_set_recording_destination_first_frame_ts_filepath(&encoder, video_recording_destination_id, ts_filepath.c_str()); - } - - if(video_recording_destination_id != (size_t)-1) - replay_recording_items.push_back(video_recording_destination_id); - - for(const auto &audio_input : replay_recording_start_result.audio_inputs) { - const size_t audio_recording_destination_id = gsr_encoder_add_recording_destination(&encoder, audio_input.audio_track->codec_context, replay_recording_start_result.av_format_context, audio_input.stream, audio_input.audio_track->pts); - if(audio_recording_destination_id != (size_t)-1) - replay_recording_items.push_back(audio_recording_destination_id); - } - - replay_recording = true; - force_iframe_frame = true; - fprintf(stderr, "Started recording\n"); - } else { - printf("gsr error: Failed to start recording\n"); - fflush(stdout); - } - } else if(replay_recording_start_result.av_format_context) { - for(size_t id : replay_recording_items) { - gsr_encoder_remove_recording_destination(&encoder, id); - } - replay_recording_items.clear(); - - if(stop_recording_close_streams(replay_recording_start_result.av_format_context)) { - fprintf(stderr, "Stopped recording\n"); - puts(replay_recording_filepath.c_str()); - fflush(stdout); - if(arg_parser.recording_saved_script) - run_recording_saved_script_async(arg_parser.recording_saved_script, replay_recording_filepath.c_str(), "regular"); - } else { - printf("gsr error: Failed to save recording\n"); - fflush(stdout); - } - - replay_recording_start_result = RecordingStartResult{}; - replay_recording = false; - replay_recording_filepath.clear(); - } - } - - if(save_replay_thread.valid() && save_replay_thread.wait_for(std::chrono::seconds(0)) == std::future_status::ready) { - const bool replay_save_result = save_replay_thread.get(); - if(save_replay_output_filepath.empty() || !replay_save_result) { - printf("gsr error: Failed to save replay\n"); - fflush(stdout); - } else { - puts(save_replay_output_filepath.c_str()); - fflush(stdout); - if(arg_parser.recording_saved_script) - run_recording_saved_script_async(arg_parser.recording_saved_script, save_replay_output_filepath.c_str(), "replay"); - } - } - - if(save_replay_seconds != 0 && !save_replay_thread.valid() && arg_parser.is_replaying) { - int current_save_replay_seconds = save_replay_seconds; - if(current_save_replay_seconds > 0) - current_save_replay_seconds += arg_parser.keyint; - - save_replay_seconds = 0; - save_replay_output_filepath.clear(); - const bool replay_start_result = save_replay_async(video_codec_context, VIDEO_STREAM_INDEX, audio_tracks, &encoder, arg_parser, file_extension, arg_parser.date_folders, hdr, video_sources, current_save_replay_seconds); - if(!replay_start_result) { - printf("gsr error: Failed to save replay\n"); - fflush(stdout); - } - - if(arg_parser.restart_replay_on_save && current_save_replay_seconds == save_replay_seconds_full) { - pthread_mutex_lock(&encoder.replay_mutex); - gsr_replay_buffer_clear(encoder.replay_buffer); - pthread_mutex_unlock(&encoder.replay_mutex); - } - } - - const double time_at_frame_end = clock_get_monotonic_seconds() - paused_time_offset; - const double time_elapsed_total = time_at_frame_end - record_start_time; - const int64_t frames_elapsed = std::floor(time_elapsed_total / target_fps); - const double time_at_next_frame = (frames_elapsed + 1) * target_fps; - double time_to_next_frame = time_at_next_frame - time_elapsed_total; - if(time_to_next_frame > target_fps) - time_to_next_frame = target_fps; - const int64_t end_num_missed_frames = frames_elapsed - video_pts_counter; - - if(time_to_next_frame > 0.0 && end_num_missed_frames <= 0) - av_usleep(time_to_next_frame * 1000.0 * 1000.0); - else { - if(paused) - av_usleep(20.0 * 1000.0); // 20 milliseconds - else if(arg_parser.framerate_mode == GSR_FRAMERATE_MODE_CONTENT) - av_usleep(2.8 * 1000.0); // 2.8 milliseconds - } - } - - running = 0; - - if(save_replay_thread.valid()) { - save_replay_thread.get(); - if(save_replay_output_filepath.empty()) { - // TODO: Output failed to save - } else { - puts(save_replay_output_filepath.c_str()); - fflush(stdout); - if(arg_parser.recording_saved_script) - run_recording_saved_script_async(arg_parser.recording_saved_script, save_replay_output_filepath.c_str(), "replay"); - } - } - - gsr_plugins_deinit(&plugins); - - if(replay_recording_start_result.av_format_context) { - for(size_t id : replay_recording_items) { - gsr_encoder_remove_recording_destination(&encoder, id); - } - replay_recording_items.clear(); - - if(stop_recording_close_streams(replay_recording_start_result.av_format_context)) { - fprintf(stderr, "Stopped recording\n"); - puts(replay_recording_filepath.c_str()); - fflush(stdout); - if(arg_parser.recording_saved_script) - run_recording_saved_script_async(arg_parser.recording_saved_script, replay_recording_filepath.c_str(), "regular"); - } else { - printf("gsr error: Failed to save recording\n"); - fflush(stdout); - } - } - - for(AudioTrack &audio_track : audio_tracks) { - for(auto &audio_device : audio_track.audio_devices) { - audio_device.thread.join(); - sound_device_close(&audio_device.sound_device); - } - } - - if(amix_thread.joinable()) - amix_thread.join(); - - // TODO: Replace this with start_recording_create_steams - if(!arg_parser.is_replaying && av_write_trailer(av_format_context) != 0) { - //fprintf(stderr, "Failed to write trailer\n"); - } - - if(!arg_parser.is_replaying && !(output_format->flags & AVFMT_NOFILE)) { - avio_close(av_format_context->pb); - avformat_free_context(av_format_context); - } - - gsr_cursor_deinit(&x11_cursor); - gsr_damage_deinit(&damage); - gsr_color_conversion_deinit(&color_conversion); - gsr_video_encoder_destroy(video_encoder, video_codec_context); - gsr_encoder_deinit(&encoder); - for(VideoSource &video_source : video_sources) { - gsr_capture_destroy(video_source.capture); - } -#ifdef GSR_APP_AUDIO - gsr_pipewire_audio_deinit(&pipewire_audio); -#endif - if(kms_client_initialized) { - gsr_capture_kms_cleanup_kms_fds(); - gsr_kms_client_deinit(&kms_client); - } - - gsr_kde_night_light_destroy(kde_night_light); - - if(!arg_parser.is_replaying && arg_parser.recording_saved_script) - run_recording_saved_script_async(arg_parser.recording_saved_script, arg_parser.filename, "regular"); - - if(dpy) { - // TODO: This causes a crash, why? maybe some other library dlclose xlib and that also happened to unload this??? - //XCloseDisplay(dpy); - } - - //gsr_egl_unload(&egl); - //gsr_window_destroy(&window); - - //av_frame_free(&video_frame); - free(empty_audio); - args_parser_deinit(&arg_parser); - // We do an _exit here because cuda uses at_exit to do _something_ that causes the program to freeze, - // but only on some nvidia driver versions on some gpus (RTX?), and _exit exits the program without calling - // the at_exit registered functions. - // Cuda (cuvid library in this case) seems to be waiting for a thread that never finishes execution. - // Maybe this happens because we dont clean up all ffmpeg resources? - // TODO: Investigate this. - _exit(should_stop_error ? 3 : 0); -} diff --git a/src/pipewire_audio.c b/src/pipewire_audio.c index ec4fde1..8f40695 100644 --- a/src/pipewire_audio.c +++ b/src/pipewire_audio.c @@ -1,4 +1,6 @@ #include "../include/pipewire_audio.h" +#include "../include/log.h" +#include "../include/utils.h" #include <pipewire/pipewire.h> #include <pipewire/extensions/metadata.h> @@ -388,7 +390,7 @@ static bool gsr_pipewire_audio_listen_on_metadata(gsr_pipewire_audio *self, uint self->metadata_proxy = pw_registry_bind(self->registry, id, PW_TYPE_INTERFACE_Metadata, PW_VERSION_METADATA, 0); if(!self->metadata_proxy) { - fprintf(stderr, "gsr error: gsr_pipewire_audio_listen_on_metadata: failed to bind to registry\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_pipewire_audio_listen_on_metadata: failed to bind to registry"); return false; } @@ -399,24 +401,6 @@ static bool gsr_pipewire_audio_listen_on_metadata(gsr_pipewire_audio *self, uint return true; } -static bool array_ensure_capacity(void **array, size_t size, size_t *capacity_items, size_t element_size) { - if(size + 1 >= *capacity_items) { - size_t new_capacity_items = *capacity_items * 2; - if(new_capacity_items == 0) - new_capacity_items = 32; - - void *new_data = realloc(*array, new_capacity_items * element_size); - if(!new_data) { - fprintf(stderr, "gsr error: pipewire_audio: failed to reallocate memory\n"); - return false; - } - - *array = new_data; - *capacity_items = new_capacity_items; - } - return true; -} - struct gsr_pipewire_audio_node_binding { gsr_pipewire_audio *self; struct pw_proxy *proxy; @@ -434,7 +418,7 @@ static gsr_pipewire_audio_node* gsr_pipewire_audio_get_node_by_id(gsr_pipewire_a } static void gsr_pipewire_audio_add_node(gsr_pipewire_audio *self, uint32_t id, const char *node_name, gsr_pipewire_audio_node_type type, bool is_virtual, bool is_application) { - if(!array_ensure_capacity((void**)&self->stream_nodes, self->num_stream_nodes, &self->stream_nodes_capacity_items, sizeof(gsr_pipewire_audio_node))) + if(!gsr_array_ensure_capacity((void**)&self->stream_nodes, self->num_stream_nodes, &self->stream_nodes_capacity_items, sizeof(gsr_pipewire_audio_node))) return; char *node_name_copy = strdup(node_name); @@ -524,7 +508,7 @@ static const struct pw_proxy_events node_proxy_events = { /* The registry only broadcasts a small set of the node properties (node.name, media.class, application.name, etc). Binding to the node is required to get the other properties (node.virtual, application.id, etc) from the node info event. */ static void gsr_pipewire_audio_bind_node(gsr_pipewire_audio *self, uint32_t id) { - if(!array_ensure_capacity((void**)&self->node_bindings, self->num_node_bindings, &self->node_bindings_capacity_items, sizeof(gsr_pipewire_audio_node_binding*))) + if(!gsr_array_ensure_capacity((void**)&self->node_bindings, self->num_node_bindings, &self->node_bindings_capacity_items, sizeof(gsr_pipewire_audio_node_binding*))) return; gsr_pipewire_audio_node_binding *node_binding = calloc(1, sizeof(gsr_pipewire_audio_node_binding)); @@ -533,7 +517,7 @@ static void gsr_pipewire_audio_bind_node(gsr_pipewire_audio *self, uint32_t id) struct pw_proxy *proxy = pw_registry_bind(self->registry, id, PW_TYPE_INTERFACE_Node, PW_VERSION_NODE, 0); if(!proxy) { - fprintf(stderr, "gsr error: gsr_pipewire_audio_bind_node: failed to bind to node %u\n", id); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_pipewire_audio_bind_node: failed to bind to node %u", id); free(node_binding); return; } @@ -591,7 +575,7 @@ static void registry_event_global(void *data, uint32_t id, uint32_t permissions, const int node_id_num = node_id ? atoi(node_id) : 0; if(port_name && direction >= 0 && node_id_num > 0) { - if(!array_ensure_capacity((void**)&self->ports, self->num_ports, &self->ports_capacity_items, sizeof(gsr_pipewire_audio_port))) + if(!gsr_array_ensure_capacity((void**)&self->ports, self->num_ports, &self->ports_capacity_items, sizeof(gsr_pipewire_audio_port))) return; //fprintf(stderr, " port name: %s, node id: %d, direction: %s\n", port_name, node_id_num, port_direction); @@ -614,7 +598,7 @@ static void registry_event_global(void *data, uint32_t id, uint32_t permissions, const uint32_t output_node_id_num = output_node ? atoi(output_node) : 0; const uint32_t input_node_id_num = input_node ? atoi(input_node) : 0; if(output_node_id_num > 0 && input_node_id_num > 0) { - if(!array_ensure_capacity((void**)&self->links, self->num_links, &self->links_capacity_items, sizeof(gsr_pipewire_audio_link))) + if(!gsr_array_ensure_capacity((void**)&self->links, self->num_links, &self->links_capacity_items, sizeof(gsr_pipewire_audio_link))) return; //fprintf(stderr, " new link (%u): %u -> %u\n", id, output_node_id_num, input_node_id_num); @@ -701,14 +685,14 @@ bool gsr_pipewire_audio_init(gsr_pipewire_audio *self) { self->thread_loop = pw_thread_loop_new("gsr screen capture", NULL); if(!self->thread_loop) { - fprintf(stderr, "gsr error: gsr_pipewire_audio_init: failed to create pipewire thread\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_pipewire_audio_init: failed to create pipewire thread"); gsr_pipewire_audio_deinit(self); return false; } self->context = pw_context_new(pw_thread_loop_get_loop(self->thread_loop), NULL, 0); if(!self->context) { - fprintf(stderr, "gsr error: gsr_pipewire_audio_init: failed to create pipewire context\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_pipewire_audio_init: failed to create pipewire context"); gsr_pipewire_audio_deinit(self); return false; } @@ -716,7 +700,7 @@ bool gsr_pipewire_audio_init(gsr_pipewire_audio *self) { pw_context_load_module(self->context, "libpipewire-module-link-factory", NULL, NULL); if(pw_thread_loop_start(self->thread_loop) < 0) { - fprintf(stderr, "gsr error: gsr_pipewire_audio_init: failed to start thread\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_pipewire_audio_init: failed to start thread"); gsr_pipewire_audio_deinit(self); return false; } @@ -813,6 +797,11 @@ void gsr_pipewire_audio_deinit(gsr_pipewire_audio *self) { spa_hook_remove(&self->registry_listener); spa_hook_remove(&self->core_listener); + if(self->registry) { + pw_proxy_destroy((struct pw_proxy*)self->registry); + self->registry = NULL; + } + if(self->core) { pw_core_disconnect(self->core); self->core = NULL; @@ -890,7 +879,7 @@ static bool string_remove_suffix(char *str, const char *suffix) { } static bool gsr_pipewire_audio_add_links_to_output(gsr_pipewire_audio *self, const char **output_names, int num_output_names, const char *input_name, gsr_pipewire_audio_node_type output_type, gsr_pipewire_audio_link_input_type input_type, bool inverted) { - if(!array_ensure_capacity((void**)&self->requested_links, self->num_requested_links, &self->requested_links_capacity_items, sizeof(gsr_pipewire_audio_requested_link))) + if(!gsr_array_ensure_capacity((void**)&self->requested_links, self->num_requested_links, &self->requested_links_capacity_items, sizeof(gsr_pipewire_audio_requested_link))) return false; gsr_pipewire_audio_requested_output *outputs = calloc(num_output_names, sizeof(gsr_pipewire_audio_requested_output)); diff --git a/src/pipewire_video.c b/src/pipewire_video.c index 18f6c9a..80cef7e 100644 --- a/src/pipewire_video.c +++ b/src/pipewire_video.c @@ -1,4 +1,5 @@ #include "../include/pipewire_video.h" +#include "../include/log.h" #include "../include/egl.h" #include "../include/utils.h" @@ -62,11 +63,11 @@ static bool check_pw_version(const gsr_pipewire_video_data_version *pw_version, } static void update_pw_versions(gsr_pipewire_video *self, const char *version) { - fprintf(stderr, "gsr info: pipewire: server version: %s\n", version); - fprintf(stderr, "gsr info: pipewire: library version: %s\n", pw_get_library_version()); - fprintf(stderr, "gsr info: pipewire: header version: %s\n", pw_get_headers_version()); + gsr_log(GSR_LOG_LEVEL_INFO, "pipewire: server version: %s", version); + gsr_log(GSR_LOG_LEVEL_INFO, "pipewire: library version: %s", pw_get_library_version()); + gsr_log(GSR_LOG_LEVEL_INFO, "pipewire: header version: %s", pw_get_headers_version()); if(!parse_pw_version(&self->server_version, version)) - fprintf(stderr, "gsr error: pipewire: failed to parse server version\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "pipewire: failed to parse server version"); } static void on_core_info_cb(void *user_data, const struct pw_core_info *info) { @@ -76,7 +77,7 @@ static void on_core_info_cb(void *user_data, const struct pw_core_info *info) { static void on_core_error_cb(void *user_data, uint32_t id, int seq, int res, const char *message) { gsr_pipewire_video *self = user_data; - fprintf(stderr, "gsr error: pipewire: error id:%u seq:%d res:%d: %s\n", id, seq, res, message); + gsr_log(GSR_LOG_LEVEL_ERROR, "pipewire: error id:%u seq:%d res:%d: %s", id, seq, res, message); pw_thread_loop_signal(self->thread_loop, false); } @@ -138,8 +139,7 @@ static void gsr_pipewire_video_read_cursor_metadata(gsr_pipewire_video *self, st if(bitmap && bitmap->size.width > 0 && bitmap->size.height > 0 && is_cursor_format_supported(bitmap->format)) { /* Animated cursors update the bitmap for every animation frame, only log when the size changes */ if((int)bitmap->size.width != self->cursor.width || (int)bitmap->size.height != self->cursor.height) { - fprintf(stderr, "gsr info: pipewire: cursor bitmap update, size: %dx%d, format: %s\n", - (int)bitmap->size.width, (int)bitmap->size.height, spa_debug_type_find_name(spa_type_video_format, bitmap->format)); + gsr_log(GSR_LOG_LEVEL_INFO, "pipewire: cursor bitmap update, size: %dx%d, format: %s", (int)bitmap->size.width, (int)bitmap->size.height, spa_debug_type_find_name(spa_type_video_format, bitmap->format)); } const uint8_t *bitmap_data = SPA_MEMBER(bitmap, bitmap->offset, uint8_t); @@ -205,7 +205,7 @@ static void on_process_cb(void *user_data) { } if(!got_buffer) { - fprintf(stderr, "gsr info: pipewire: out of buffers!\n"); + gsr_log(GSR_LOG_LEVEL_INFO, "pipewire: out of buffers!"); return; } @@ -315,19 +315,18 @@ static void on_param_changed_cb(void *user_data, uint32_t id, const struct spa_p if(has_modifier || check_pw_version(&self->server_version, 0, 3, 24)) buffer_types |= 1 << SPA_DATA_DmaBuf; - fprintf(stderr, "gsr info: pipewire: negotiated format:\n"); + gsr_log(GSR_LOG_LEVEL_INFO, "pipewire: negotiated format:"); - fprintf(stderr, "gsr info: pipewire: Format: %d (%s)\n", - self->format.info.raw.format, + gsr_log(GSR_LOG_LEVEL_INFO, "pipewire: Format: %d (%s)", self->format.info.raw.format, spa_debug_type_find_name(spa_type_video_format, self->format.info.raw.format)); self->has_modifier = has_modifier; if(self->has_modifier) { - fprintf(stderr, "gsr info: pipewire: Modifier: 0x%" PRIx64 "\n", self->format.info.raw.modifier); + gsr_log(GSR_LOG_LEVEL_INFO, "pipewire: Modifier: 0x%" PRIx64, self->format.info.raw.modifier); } - fprintf(stderr, "gsr info: pipewire: Size: %dx%d\n", self->format.info.raw.size.width, self->format.info.raw.size.height); - fprintf(stderr, "gsr info: pipewire: Framerate: %d/%d\n", self->format.info.raw.framerate.num, self->format.info.raw.framerate.denom); + gsr_log(GSR_LOG_LEVEL_INFO, "pipewire: Size: %dx%d", self->format.info.raw.size.width, self->format.info.raw.size.height); + gsr_log(GSR_LOG_LEVEL_INFO, "pipewire: Framerate: %d/%d", self->format.info.raw.framerate.num, self->format.info.raw.framerate.denom); uint8_t params_buffer[2048]; struct spa_pod_builder pod_builder = SPA_POD_BUILDER_INIT(params_buffer, sizeof(params_buffer)); @@ -378,14 +377,11 @@ static void on_param_changed_cb(void *user_data, uint32_t id, const struct spa_p static void on_state_changed_cb(void *user_data, enum pw_stream_state prev_state, enum pw_stream_state new_state, const char *error) { gsr_pipewire_video *self = user_data; - fprintf(stderr, "gsr info: pipewire: stream %p previous state: \"%s\", new state: \"%s\" (error: %s)\n", - (void*)self->stream, pw_stream_state_as_string(prev_state), pw_stream_state_as_string(new_state), + gsr_log(GSR_LOG_LEVEL_INFO, "pipewire: stream %p previous state: \"%s\", new state: \"%s\" (error: %s)", (void*)self->stream, pw_stream_state_as_string(prev_state), pw_stream_state_as_string(new_state), error ? error : "none"); if(new_state == PW_STREAM_STATE_ERROR && error && strstr(error, "alloc buffers")) { - fprintf(stderr, "gsr error: pipewire: the desktop portal failed to provide dmabuf (GPU) video frames. This happens when the " - "compositor only provides the frames as shared memory (CPU), which gpu-screen-recorder intentionally doesn't support for portal capture. " - "Capture the monitor directly (for example '-w screen' or '-w <monitor-name>', see 'gpu-screen-recorder --list-capture-options') instead of '-w portal'.\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "pipewire: the desktop portal failed to provide dmabuf (GPU) video frames. This happens when the compositor only provides the frames as shared memory (CPU), which gpu-screen-recorder intentionally doesn't support for portal capture. Capture the monitor directly (for example '-w screen' or '-w <monitor-name>', see 'gpu-screen-recorder --list-capture-options') instead of '-w portal'."); } pthread_mutex_lock(&self->mutex); @@ -528,7 +524,7 @@ static void renegotiate_format(void *data, uint64_t expirations) { uint8_t params_buffer[8192]; struct spa_pod_builder pod_builder = SPA_POD_BUILDER_INIT(params_buffer, sizeof(params_buffer)); if (!gsr_pipewire_video_build_format_params(self, &pod_builder, params, &num_video_formats)) { - fprintf(stderr, "gsr error: renegotiate_format: failed to build formats\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "renegotiate_format: failed to build formats"); pw_thread_loop_unlock(self->thread_loop); return; } @@ -541,7 +537,7 @@ static bool spa_video_format_get_modifiers(gsr_pipewire_video *self, const enum *num_modifiers = 0; if(max_modifiers == 0) { - fprintf(stderr, "gsr error: spa_video_format_get_modifiers: no space for modifiers left\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "spa_video_format_get_modifiers: no space for modifiers left"); //modifiers[0] = DRM_FORMAT_MOD_LINEAR; //modifiers[1] = DRM_FORMAT_MOD_INVALID; //*num_modifiers = 2; @@ -549,7 +545,7 @@ static bool spa_video_format_get_modifiers(gsr_pipewire_video *self, const enum } if(!self->egl->eglQueryDmaBufModifiersEXT) { - fprintf(stderr, "gsr error: spa_video_format_get_modifiers: failed to initialize modifiers because eglQueryDmaBufModifiersEXT is not available\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "spa_video_format_get_modifiers: failed to initialize modifiers because eglQueryDmaBufModifiersEXT is not available"); //modifiers[0] = DRM_FORMAT_MOD_LINEAR; //modifiers[1] = DRM_FORMAT_MOD_INVALID; //*num_modifiers = 2; @@ -560,12 +556,12 @@ static bool spa_video_format_get_modifiers(gsr_pipewire_video *self, const enum const int64_t drm_format = spa_video_format_to_drm_format(format); if(drm_format == DRM_FORMAT_INVALID) { - fprintf(stderr, "gsr error: spa_video_format_get_modifiers: unsupported format: %d\n", (int)format); + gsr_log(GSR_LOG_LEVEL_ERROR, "spa_video_format_get_modifiers: unsupported format: %d", (int)format); return false; } if(!self->egl->eglQueryDmaBufModifiersEXT(self->egl->egl_display, drm_format, max_modifiers, modifiers, NULL, num_modifiers)) { - fprintf(stderr, "gsr error: spa_video_format_get_modifiers: eglQueryDmaBufModifiersEXT failed with drm format %d, %" PRIi64 "\n", (int)format, drm_format); + gsr_log(GSR_LOG_LEVEL_ERROR, "spa_video_format_get_modifiers: eglQueryDmaBufModifiersEXT failed with drm format %d, %" PRIi64, (int)format, drm_format); modifiers[0] = DRM_FORMAT_MOD_INVALID; *num_modifiers = 1; return false; @@ -618,18 +614,18 @@ static bool gsr_pipewire_video_setup_stream(gsr_pipewire_video *self) { self->thread_loop = pw_thread_loop_new("gsr screen capture", NULL); if(!self->thread_loop) { - fprintf(stderr, "gsr error: gsr_pipewire_video_setup_stream: failed to create pipewire thread\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_pipewire_video_setup_stream: failed to create pipewire thread"); goto error; } self->context = pw_context_new(pw_thread_loop_get_loop(self->thread_loop), NULL, 0); if(!self->context) { - fprintf(stderr, "gsr error: gsr_pipewire_video_setup_stream: failed to create pipewire context\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_pipewire_video_setup_stream: failed to create pipewire context"); goto error; } if(pw_thread_loop_start(self->thread_loop) < 0) { - fprintf(stderr, "gsr error: gsr_pipewire_video_setup_stream: failed to start thread\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_pipewire_video_setup_stream: failed to start thread"); goto error; } @@ -639,7 +635,7 @@ static bool gsr_pipewire_video_setup_stream(gsr_pipewire_video *self) { self->core = pw_context_connect_fd(self->context, fcntl(self->fd, F_DUPFD_CLOEXEC, 5), NULL, 0); if(!self->core) { pw_thread_loop_unlock(self->thread_loop); - fprintf(stderr, "gsr error: gsr_pipewire_video_setup_stream: failed to connect to fd %d\n", self->fd); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_pipewire_video_setup_stream: failed to connect to fd %d", self->fd); goto error; } @@ -655,7 +651,7 @@ static bool gsr_pipewire_video_setup_stream(gsr_pipewire_video *self) { self->reneg = pw_loop_add_event(pw_thread_loop_get_loop(self->thread_loop), renegotiate_format, self); if(!self->reneg) { pw_thread_loop_unlock(self->thread_loop); - fprintf(stderr, "gsr error: gsr_pipewire_video_setup_stream: pw_loop_add_event failed\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_pipewire_video_setup_stream: pw_loop_add_event failed"); goto error; } @@ -665,14 +661,14 @@ static bool gsr_pipewire_video_setup_stream(gsr_pipewire_video *self) { PW_KEY_MEDIA_ROLE, "Screen", NULL)); if(!self->stream) { pw_thread_loop_unlock(self->thread_loop); - fprintf(stderr, "gsr error: gsr_pipewire_video_setup_stream: failed to create stream\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_pipewire_video_setup_stream: failed to create stream"); goto error; } pw_stream_add_listener(self->stream, &self->stream_listener, &stream_events, self); if(!gsr_pipewire_video_build_format_params(self, &pod_builder, params, &num_video_formats)) { pw_thread_loop_unlock(self->thread_loop); - fprintf(stderr, "gsr error: gsr_pipewire_video_setup_stream: failed to build format params\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_pipewire_video_setup_stream: failed to build format params"); goto error; } @@ -682,7 +678,7 @@ static bool gsr_pipewire_video_setup_stream(gsr_pipewire_video *self) { num_video_formats) < 0) { pw_thread_loop_unlock(self->thread_loop); - fprintf(stderr, "gsr error: gsr_pipewire_video_setup_stream: failed to connect stream\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_pipewire_video_setup_stream: failed to connect stream"); goto error; } @@ -729,7 +725,7 @@ bool gsr_pipewire_video_init(gsr_pipewire_video *self, int pipewire_fd, uint32_t self->fd = pipewire_fd; self->node = pipewire_node; if(pthread_mutex_init(&self->mutex, NULL) != 0) { - fprintf(stderr, "gsr error: gsr_pipewire_video_init: failed to initialize mutex\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_pipewire_video_init: failed to initialize mutex"); gsr_pipewire_video_deinit(self); return false; } @@ -839,11 +835,11 @@ static EGLImage gsr_pipewire_video_create_egl_image_with_fallback(gsr_pipewire_v image = gsr_pipewire_video_create_egl_image(self, fds, offsets, pitches, modifiers, true); if(!image) { if(self->format.info.raw.modifier == DRM_FORMAT_MOD_INVALID) { - fprintf(stderr, "gsr error: gsr_pipewire_video_create_egl_image_with_fallback: failed to create egl image with modifiers, trying without modifiers\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_pipewire_video_create_egl_image_with_fallback: failed to create egl image with modifiers, trying without modifiers"); self->no_modifiers_fallback = true; image = gsr_pipewire_video_create_egl_image(self, fds, offsets, pitches, modifiers, false); } else { - fprintf(stderr, "gsr error: gsr_pipewire_video_create_egl_image_with_fallback: failed to create egl image with modifier 0x%" PRIx64 ", renegotiating with a different modifier\n", self->format.info.raw.modifier); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_pipewire_video_create_egl_image_with_fallback: failed to create egl image with modifier 0x%" PRIx64 ", renegotiating with a different modifier", self->format.info.raw.modifier); self->negotiated = false; pw_thread_loop_lock(self->thread_loop); gsr_pipewire_video_remove_modifier(self, self->format.info.raw.modifier); @@ -870,7 +866,7 @@ static void gsr_pipewire_video_bind_image_to_texture_with_fallback(gsr_pipewire_ gsr_pipewire_video_bind_image_to_texture(self, image, texture_map.external_texture_id, true); } else { if(!gsr_pipewire_video_bind_image_to_texture(self, image, texture_map.texture_id, false)) { - fprintf(stderr, "gsr error: gsr_pipewire_video_map_texture: failed to bind image to texture, trying with external texture\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_pipewire_video_map_texture: failed to bind image to texture, trying with external texture"); self->external_texture_fallback = true; gsr_pipewire_video_bind_image_to_texture(self, image, texture_map.external_texture_id, true); } diff --git a/src/plugins.c b/src/plugins.c index 053b2a3..5b9fd8c 100644 --- a/src/plugins.c +++ b/src/plugins.c @@ -1,6 +1,6 @@ #include "../include/plugins.h" +#include "../include/log.h" #include "../include/utils.h" -#include <stdio.h> #include <string.h> #include <dlfcn.h> #include <assert.h> @@ -23,7 +23,7 @@ bool gsr_plugins_init(gsr_plugins *self, gsr_plugin_init_params init_params, gsr const unsigned int texture = gl_create_texture(egl, init_params.width, init_params.height, color_depth_to_gl_internal_format(init_params.color_depth), GL_RGBA, GL_LINEAR); if(texture == 0) { - fprintf(stderr, "gsr error: gsr_plugins_init failed to create texture\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_plugins_init failed to create texture"); return false; } self->texture = texture; @@ -41,7 +41,7 @@ bool gsr_plugins_init(gsr_plugins *self, gsr_plugin_init_params init_params, gsr color_conversion_params.destination_textures[0] = self->texture; if(gsr_color_conversion_init(&self->color_conversion, &color_conversion_params) != 0) { - fprintf(stderr, "gsr error: gsr_plugins_init failed to create color conversion\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_plugins_init failed to create color conversion"); gsr_plugins_deinit(self); return false; } @@ -54,7 +54,7 @@ void gsr_plugins_deinit(gsr_plugins *self) { for(int i = self->num_plugins - 1; i >= 0; --i) { gsr_plugin *plugin = &self->plugins[i]; plugin->gsr_plugin_deinit(plugin->data.userdata); - fprintf(stderr, "gsr info: unloaded plugin: %s\n", plugin->data.name); + gsr_log(GSR_LOG_LEVEL_INFO, "unloaded plugin: %s", plugin->data.name); } self->num_plugins = 0; @@ -68,7 +68,7 @@ void gsr_plugins_deinit(gsr_plugins *self) { bool gsr_plugins_load_plugin(gsr_plugins *self, const char *plugin_filepath) { if(self->num_plugins >= GSR_MAX_PLUGINS) { - fprintf(stderr, "gsr error: gsr_plugins_load_plugin failed, more plugins can't load more than %d plugins. Report this as an issue\n", GSR_MAX_PLUGINS); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_plugins_load_plugin failed, more plugins can't load more than %d plugins. Report this as an issue", GSR_MAX_PLUGINS); return false; } @@ -77,38 +77,38 @@ bool gsr_plugins_load_plugin(gsr_plugins *self, const char *plugin_filepath) { plugin.lib = dlopen(plugin_filepath, RTLD_LAZY); if(!plugin.lib) { - fprintf(stderr, "gsr error: gsr_plugins_load_plugin failed to load \"%s\", error: %s\n", plugin_filepath, dlerror()); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_plugins_load_plugin failed to load \"%s\", error: %s", plugin_filepath, dlerror()); return false; } plugin.gsr_plugin_init = dlsym(plugin.lib, "gsr_plugin_init"); if(!plugin.gsr_plugin_init) { - fprintf(stderr, "gsr error: gsr_plugins_load_plugin failed to find \"gsr_plugin_init\" in plugin \"%s\"\n", plugin_filepath); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_plugins_load_plugin failed to find \"gsr_plugin_init\" in plugin \"%s\"", plugin_filepath); goto fail; } plugin.gsr_plugin_deinit = dlsym(plugin.lib, "gsr_plugin_deinit"); if(!plugin.gsr_plugin_deinit) { - fprintf(stderr, "gsr error: gsr_plugins_load_plugin failed to find \"gsr_plugin_deinit\" in plugin \"%s\"\n", plugin_filepath); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_plugins_load_plugin failed to find \"gsr_plugin_deinit\" in plugin \"%s\"", plugin_filepath); goto fail; } if(!plugin.gsr_plugin_init(&self->init_params, &plugin.data)) { - fprintf(stderr, "gsr error: gsr_plugins_load_plugin failed to load plugin \"%s\", gsr_plugin_init in the plugin failed\n", plugin_filepath); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_plugins_load_plugin failed to load plugin \"%s\", gsr_plugin_init in the plugin failed", plugin_filepath); goto fail; } if(!plugin.data.name) { - fprintf(stderr, "gsr error: gsr_plugins_load_plugin failed to load plugin \"%s\", the plugin didn't set the name (gsr_plugin_init_return.name)\n", plugin_filepath); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_plugins_load_plugin failed to load plugin \"%s\", the plugin didn't set the name (gsr_plugin_init_return.name)", plugin_filepath); goto fail; } if(plugin.data.version == 0) { - fprintf(stderr, "gsr error: gsr_plugins_load_plugin failed to load plugin \"%s\", the plugin didn't set the version (gsr_plugin_init_return.version)\n", plugin_filepath); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_plugins_load_plugin failed to load plugin \"%s\", the plugin didn't set the version (gsr_plugin_init_return.version)", plugin_filepath); goto fail; } - fprintf(stderr, "gsr info: loaded plugin: %s, name: %s, version: %u\n", plugin_filepath, plugin.data.name, plugin.data.version); + gsr_log(GSR_LOG_LEVEL_INFO, "loaded plugin: %s, name: %s, version: %u", plugin_filepath, plugin.data.name, plugin.data.version); self->plugins[self->num_plugins] = plugin; ++self->num_plugins; return true; diff --git a/src/recorder/audio_capture.c b/src/recorder/audio_capture.c new file mode 100644 index 0000000..6275c93 --- /dev/null +++ b/src/recorder/audio_capture.c @@ -0,0 +1,610 @@ +#include "../../include/recorder/audio_capture.h" +#include "../../include/recorder/error.h" +#include "../../include/recorder/audio_codec.h" +#include "../../include/utils.h" +#include "../../include/log.h" + +#include <string.h> +#include <stdlib.h> +#include <stdio.h> +#include <math.h> + +#include <libavutil/opt.h> +#include <libavutil/time.h> +#include <libswresample/swresample.h> +#include <libavfilter/buffersink.h> +#include <libavfilter/buffersrc.h> + +int gsr_audio_init_filter_graph(AVCodecContext *audio_codec_context, AVFilterGraph **graph, AVFilterContext **sink, AVFilterContext **src_filter_ctx, size_t num_sources) { + char ch_layout[64]; + int err = 0; + ch_layout[0] = '\0'; + + // C89-style variable declaration to + // avoid problems because of goto + AVFilterGraph* filter_graph = NULL; + AVFilterContext* mix_ctx = NULL; + + const AVFilter* mix_filter = NULL; + const AVFilter* abuffersink = NULL; + AVFilterContext* abuffersink_ctx = NULL; + char args[512] = { 0 }; +#if LIBAVFILTER_VERSION_INT >= AV_VERSION_INT(7, 107, 100) + bool normalize = false; +#endif + + filter_graph = avfilter_graph_alloc(); + if (!filter_graph) { + gsr_log(GSR_LOG_LEVEL_ERROR, "Unable to create filter graph"); + err = AVERROR(ENOMEM); + goto fail; + } + + for(size_t i = 0; i < num_sources; ++i) { + const AVFilter *abuffer = avfilter_get_by_name("abuffer"); + if (!abuffer) { + gsr_log(GSR_LOG_LEVEL_ERROR, "Could not find the abuffer filter"); + err = AVERROR_FILTER_NOT_FOUND; + goto fail; + } + + AVFilterContext *abuffer_ctx = avfilter_graph_alloc_filter(filter_graph, abuffer, NULL); + if (!abuffer_ctx) { + gsr_log(GSR_LOG_LEVEL_ERROR, "Could not allocate the abuffer instance"); + err = AVERROR(ENOMEM); + goto fail; + } + + #if LIBAVCODEC_VERSION_MAJOR < 60 + av_get_channel_layout_string(ch_layout, sizeof(ch_layout), 0, AV_CH_LAYOUT_STEREO); + #else + av_channel_layout_describe(&audio_codec_context->ch_layout, ch_layout, sizeof(ch_layout)); + #endif + av_opt_set (abuffer_ctx, "channel_layout", ch_layout, AV_OPT_SEARCH_CHILDREN); + av_opt_set (abuffer_ctx, "sample_fmt", av_get_sample_fmt_name(audio_codec_context->sample_fmt), AV_OPT_SEARCH_CHILDREN); + av_opt_set_q (abuffer_ctx, "time_base", audio_codec_context->time_base, AV_OPT_SEARCH_CHILDREN); + av_opt_set_int(abuffer_ctx, "sample_rate", audio_codec_context->sample_rate, AV_OPT_SEARCH_CHILDREN); + av_opt_set_int(abuffer_ctx, "bit_rate", audio_codec_context->bit_rate, AV_OPT_SEARCH_CHILDREN); + + err = avfilter_init_str(abuffer_ctx, NULL); + if (err < 0) { + gsr_log(GSR_LOG_LEVEL_ERROR, "Could not initialize the abuffer filter"); + goto fail; + } + + src_filter_ctx[i] = abuffer_ctx; + } + + mix_filter = avfilter_get_by_name("amix"); + if (!mix_filter) { + av_log(NULL, AV_LOG_ERROR, "Could not find the mix filter.\n"); + err = AVERROR_FILTER_NOT_FOUND; + goto fail; + } + +#if LIBAVFILTER_VERSION_INT >= AV_VERSION_INT(7, 107, 100) + snprintf(args, sizeof(args), "inputs=%d:normalize=%s", (int)num_sources, normalize ? "true" : "false"); +#else + snprintf(args, sizeof(args), "inputs=%d", (int)num_sources); + gsr_log(GSR_LOG_LEVEL_WARNING, "your ffmpeg version doesn't support disabling normalizing of mixed audio. Volume might be lower than expected"); +#endif + + err = avfilter_graph_create_filter(&mix_ctx, mix_filter, "amix", args, NULL, filter_graph); + if (err < 0) { + av_log(NULL, AV_LOG_ERROR, "Cannot create audio amix filter\n"); + goto fail; + } + + abuffersink = avfilter_get_by_name("abuffersink"); + if (!abuffersink) { + gsr_log(GSR_LOG_LEVEL_ERROR, "Could not find the abuffersink filter"); + err = AVERROR_FILTER_NOT_FOUND; + goto fail; + } + + abuffersink_ctx = avfilter_graph_alloc_filter(filter_graph, abuffersink, "sink"); + if (!abuffersink_ctx) { + gsr_log(GSR_LOG_LEVEL_ERROR, "Could not allocate the abuffersink instance"); + err = AVERROR(ENOMEM); + goto fail; + } + + err = avfilter_init_str(abuffersink_ctx, NULL); + if (err < 0) { + gsr_log(GSR_LOG_LEVEL_ERROR, "Could not initialize the abuffersink instance"); + goto fail; + } + + err = 0; + for(size_t i = 0; i < num_sources; ++i) { + AVFilterContext *src_ctx = src_filter_ctx[i]; + if (err >= 0) + err = avfilter_link(src_ctx, 0, mix_ctx, i); + } + if (err >= 0) + err = avfilter_link(mix_ctx, 0, abuffersink_ctx, 0); + if (err < 0) { + av_log(NULL, AV_LOG_ERROR, "Error connecting filters\n"); + goto fail; + } + + err = avfilter_graph_config(filter_graph, NULL); + if (err < 0) { + av_log(NULL, AV_LOG_ERROR, "Error configuring the filter graph\n"); + goto fail; + } + + /* Make sure the sink always outputs frames with the exact amount of samples the audio encoder wants, + otherwise the audio encoder rejects the frame and that piece of audio is lost */ + av_buffersink_set_frame_size(abuffersink_ctx, audio_codec_context->frame_size); + + *graph = filter_graph; + *sink = abuffersink_ctx; + + return 0; + +fail: + avfilter_graph_free(&filter_graph); + memset(src_filter_ctx, 0, num_sources * sizeof(AVFilterContext*)); // possibly unnecessary? + return err; +} + +static void* audio_device_thread(void *userdata) { + const gsr_audio_device_thread_userdata *thread_userdata = userdata; + gsr_audio_capture *self = thread_userdata->audio_capture; + gsr_audio_track *track = thread_userdata->track; + gsr_audio_device_capture *device = thread_userdata->device; + gsr_recording_clock *clock = self->clock; + const atomic_int *running = self->running; + + const enum AVSampleFormat sound_device_sample_format = audio_format_to_sample_format(audio_codec_context_get_audio_format(track->codec_context)); + /* TODO: Always do conversion for now. This fixes issue with stuttering audio on pulseaudio with opus + multiple audio sources merged */ + const bool needs_audio_conversion = true; + SwrContext *swr = NULL; + if(needs_audio_conversion) { + swr = swr_alloc(); + if(!swr) { + gsr_log(GSR_LOG_LEVEL_ERROR, "failed to create SwrContext"); + return NULL; + } + #if LIBAVUTIL_VERSION_MAJOR <= 56 + av_opt_set_channel_layout(swr, "in_channel_layout", AV_CH_LAYOUT_STEREO, 0); + av_opt_set_channel_layout(swr, "out_channel_layout", AV_CH_LAYOUT_STEREO, 0); + #elif LIBAVUTIL_VERSION_MAJOR >= 59 + av_opt_set_chlayout(swr, "in_chlayout", &track->codec_context->ch_layout, 0); + av_opt_set_chlayout(swr, "out_chlayout", &track->codec_context->ch_layout, 0); + #else + av_opt_set_chlayout(swr, "in_channel_layout", &track->codec_context->ch_layout, 0); + av_opt_set_chlayout(swr, "out_channel_layout", &track->codec_context->ch_layout, 0); + #endif + av_opt_set_int(swr, "in_sample_rate", track->codec_context->sample_rate, 0); + av_opt_set_int(swr, "out_sample_rate", track->codec_context->sample_rate, 0); + av_opt_set_sample_fmt(swr, "in_sample_fmt", sound_device_sample_format, 0); + av_opt_set_sample_fmt(swr, "out_sample_fmt", track->codec_context->sample_fmt, 0); + swr_init(swr); + } + + const double audio_fps = (double)track->codec_context->sample_rate / (double)track->codec_context->frame_size; + const int64_t timeout_ms = llround(1000.0 / audio_fps); + const double timeout_sec = 1000.0 / audio_fps / 1000.0; + int64_t num_received_frames = 0; + + /* The sound device is opened before the recording starts, so it can contain old audio from before the recording started. + Discard it so the recording doesn't start with old audio. */ + if(device->sound_device.handle) + sound_device_flush(&device->sound_device); + + while(atomic_load(running)) { + void *sound_buffer; + int sound_buffer_size = -1; + const double time_before_read_seconds = clock_get_monotonic_seconds(); + if(device->sound_device.handle) { + // TODO: use this instead of calculating time to read. But this can fluctuate and we dont want to go back in time, + // also it's 0.0 for some users??? + double latency_seconds = 0.0; + sound_buffer_size = sound_device_read_next_chunk(&device->sound_device, &sound_buffer, timeout_sec * 2.0, &latency_seconds); + } + + const bool got_audio_data = sound_buffer_size >= 0; + //fprintf(stderr, "got audio data: %s\n", got_audio_data ? "yes" : "no"); + //fprintf(stderr, "time to read: %f, %s, %f\n", time_to_read_seconds, got_audio_data ? "yes" : "no", timeout_sec); + const double this_audio_frame_time = gsr_recording_clock_get_time(clock); + + if(gsr_recording_clock_is_paused(clock)) { + if(!device->sound_device.handle) + av_usleep(timeout_ms * 1000); + + continue; + } + + int ret = av_frame_make_writable(device->frame); + if (ret < 0) { + gsr_log(GSR_LOG_LEVEL_ERROR, "Failed to make audio frame writable"); + break; + } + + // TODO: Is this |received_audio_time| really correct? + const int64_t num_expected_frames = floor((this_audio_frame_time - gsr_recording_clock_get_start_time(clock)) / timeout_sec); + int64_t num_missing_frames = num_expected_frames > num_received_frames ? num_expected_frames - num_received_frames : 0; + + if(got_audio_data) + num_missing_frames = num_missing_frames > 1 ? num_missing_frames - 1 : 0; + + if(!device->sound_device.handle) + num_missing_frames = num_missing_frames < 1 ? 1 : num_missing_frames; + + // Fucking hell is there a better way to do this? I JUST WANT TO KEEP VIDEO AND AUDIO SYNCED HOLY FUCK I WANT TO KILL MYSELF NOW. + // THIS PIECE OF SHIT WANTS EMPTY FRAMES OTHERWISE VIDEO PLAYS TOO FAST TO KEEP UP WITH AUDIO OR THE AUDIO PLAYS TOO EARLY. + // BUT WE CANT USE DELAYS TO GIVE DUMMY DATA BECAUSE PULSEAUDIO MIGHT GIVE AUDIO A BIG DELAYED!!! + // This garbage is needed because we want to produce constant frame rate videos instead of variable frame rate + // videos because bad software such as video editing software and VLC do not support variable frame rate software, + // despite nvidia shadowplay and xbox game bar producing variable frame rate videos. + // So we have to make sure we produce frames at the same relative rate as the video. + if((num_missing_frames >= 1 && got_audio_data) || num_missing_frames >= 5 || !device->sound_device.handle) { + // Fill the missing frames with silence. Duplicating the previous audio frame to fill the gap instead + // sounds like a stutter and it's especially noticeable at the start of the recording when the audio device + // hasn't started to deliver audio at a stable rate yet, which repeats the first audio frame multiple times. + if(needs_audio_conversion) + swr_convert(swr, &device->frame->data[0], track->codec_context->frame_size, (const uint8_t**)&self->empty_audio, track->codec_context->frame_size); + else + device->frame->data[0] = self->empty_audio; + + // TODO: Check if duplicate frame can be saved just by writing it with a different pts instead of sending it again + pthread_mutex_lock(&self->filter_mutex); + for(int i = 0; i < num_missing_frames; ++i) { + if(track->graph) { + // TODO: av_buffersrc_add_frame + if(av_buffersrc_write_frame(device->src_filter_ctx, device->frame) < 0) { + gsr_log(GSR_LOG_LEVEL_ERROR, "failed to add audio frame to filter"); + } + } else { + ret = avcodec_send_frame(track->codec_context, device->frame); + if(ret >= 0) { + // TODO: Move to separate thread because this could write to network (for example when livestreaming) + gsr_encoder_receive_packets(self->encoder, track->codec_context, device->frame->pts, track->stream_index); + } else { + gsr_log(GSR_LOG_LEVEL_ERROR, "Failed to encode audio"); + } + track->pts += track->codec_context->frame_size; + } + + device->frame->pts += track->codec_context->frame_size; + num_received_frames++; + } + pthread_mutex_unlock(&self->filter_mutex); + } + + if(!device->sound_device.handle) { + av_usleep(timeout_ms * 1000); + } else if(got_audio_data) { + // The frame has to be made writable again if the frame was already sent to the audio filter above (when filling missing frames) + // because the audio filter only references the frame data instead of copying it. Without this the sent frames data would be + // overwritten with the audio data below, causing the audio to repeat instead of the missing frames being silent. + ret = av_frame_make_writable(device->frame); + if (ret < 0) { + gsr_log(GSR_LOG_LEVEL_ERROR, "Failed to make audio frame writable"); + break; + } + + // TODO: Instead of converting audio, get float audio from alsa. Or does alsa do conversion internally to get this format? + if(needs_audio_conversion) + swr_convert(swr, &device->frame->data[0], track->codec_context->frame_size, (const uint8_t**)&sound_buffer, track->codec_context->frame_size); + else + device->frame->data[0] = (uint8_t*)sound_buffer; + + pthread_mutex_lock(&self->filter_mutex); + + if(track->graph) { + // TODO: av_buffersrc_add_frame + if(av_buffersrc_write_frame(device->src_filter_ctx, device->frame) < 0) { + gsr_log(GSR_LOG_LEVEL_ERROR, "failed to add audio frame to filter"); + } + } else { + ret = avcodec_send_frame(track->codec_context, device->frame); + if(ret >= 0) { + // TODO: Move to separate thread because this could write to network (for example when livestreaming) + gsr_encoder_receive_packets(self->encoder, track->codec_context, device->frame->pts, track->stream_index); + } else { + gsr_log(GSR_LOG_LEVEL_ERROR, "Failed to encode audio"); + } + track->pts += track->codec_context->frame_size; + } + + device->frame->pts += track->codec_context->frame_size; + num_received_frames++; + pthread_mutex_unlock(&self->filter_mutex); + } else { + // TODO: Maybe sleep for time_to_sleep_until_next_frame/4? for better latency + const double time_after_read_seconds = clock_get_monotonic_seconds(); + const double time_to_read_seconds = time_after_read_seconds - time_before_read_seconds; + const double time_to_sleep_until_next_frame = timeout_sec - time_to_read_seconds; + if(time_to_sleep_until_next_frame > 0.0) + av_usleep(time_to_sleep_until_next_frame * 1000ULL * 1000ULL); + } + } + + if(swr) + swr_free(&swr); + + return NULL; +} + +static void* amix_thread(void *userdata) { + gsr_audio_capture *self = userdata; + AVFrame *aframe = av_frame_alloc(); + while(atomic_load(self->running)) { + pthread_mutex_lock(&self->filter_mutex); + for(size_t i = 0; i < self->num_tracks; ++i) { + gsr_audio_track *track = &self->tracks[i]; + if(!track->sink) + continue; + + int err = 0; + while((err = av_buffersink_get_frame(track->sink, aframe)) >= 0) { + aframe->pts = track->pts; + err = avcodec_send_frame(track->codec_context, aframe); + if(err >= 0) { + /* TODO: Move to separate thread because this could write to network (for example when livestreaming) */ + gsr_encoder_receive_packets(self->encoder, track->codec_context, aframe->pts, track->stream_index); + } else { + gsr_log(GSR_LOG_LEVEL_ERROR, "Failed to encode audio"); + } + av_frame_unref(aframe); + track->pts += track->codec_context->frame_size; + } + } + pthread_mutex_unlock(&self->filter_mutex); + av_usleep(5 * 1000); /* 5 milliseconds */ + } + av_frame_free(&aframe); + return NULL; +} + +int gsr_audio_capture_init(gsr_audio_capture *self, gsr_encoder *encoder, gsr_recording_clock *clock, const atomic_int *running) { + memset(self, 0, sizeof(*self)); + self->encoder = encoder; + self->clock = clock; + self->running = running; + + if(pthread_mutex_init(&self->filter_mutex, NULL) != 0) { + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_audio_capture_init: failed to initialize mutex"); + return GSR_ERROR_GENERIC; + } + self->filter_mutex_initialized = true; + + return GSR_ERROR_OK; +} + +void gsr_audio_capture_deinit(gsr_audio_capture *self) { + gsr_audio_capture_join_threads(self); + + for(size_t i = 0; i < self->num_tracks; ++i) { + gsr_audio_track_deinit(&self->tracks[i]); + } + + if(self->tracks) { + free(self->tracks); + self->tracks = NULL; + } + self->num_tracks = 0; + self->capacity_tracks = 0; + + if(self->empty_audio) { + free(self->empty_audio); + self->empty_audio = NULL; + } + + if(self->filter_mutex_initialized) { + pthread_mutex_destroy(&self->filter_mutex); + self->filter_mutex_initialized = false; + } +} + +bool gsr_audio_capture_add_track(gsr_audio_capture *self, const gsr_audio_track *track) { + if(!gsr_array_ensure_capacity((void**)&self->tracks, self->num_tracks, &self->capacity_tracks, sizeof(gsr_audio_track))) + return false; + + self->tracks[self->num_tracks] = *track; + ++self->num_tracks; + return true; +} + +int gsr_audio_capture_start(gsr_audio_capture *self, int audio_max_frame_size, bool uses_amix) { + const size_t audio_buffer_size = audio_max_frame_size * 4 * 2; /* max 4 bytes/sample, 2 channels */ + self->empty_audio = calloc(1, audio_buffer_size); + if(!self->empty_audio) { + gsr_log(GSR_LOG_LEVEL_ERROR, "failed to create empty audio"); + return GSR_ERROR_GENERIC; + } + + for(size_t i = 0; i < self->num_tracks; ++i) { + gsr_audio_track *track = &self->tracks[i]; + for(size_t j = 0; j < track->num_audio_devices; ++j) { + gsr_audio_device_capture *device = &track->audio_devices[j]; + device->thread_userdata.audio_capture = self; + device->thread_userdata.track = track; + device->thread_userdata.device = device; + if(pthread_create(&device->thread, NULL, audio_device_thread, &device->thread_userdata) != 0) { + gsr_log(GSR_LOG_LEVEL_ERROR, "failed to create audio thread"); + return GSR_ERROR_GENERIC; + } + device->thread_created = true; + } + } + + if(uses_amix) { + if(pthread_create(&self->amix_thread, NULL, amix_thread, self) != 0) { + gsr_log(GSR_LOG_LEVEL_ERROR, "failed to create audio mix thread"); + return GSR_ERROR_GENERIC; + } + self->amix_thread_created = true; + } + + return GSR_ERROR_OK; +} + +void gsr_audio_capture_join_threads(gsr_audio_capture *self) { + for(size_t i = 0; i < self->num_tracks; ++i) { + gsr_audio_track *track = &self->tracks[i]; + for(size_t j = 0; j < track->num_audio_devices; ++j) { + gsr_audio_device_capture *device = &track->audio_devices[j]; + if(device->thread_created) { + pthread_join(device->thread, NULL); + device->thread_created = false; + } + } + } + + if(self->amix_thread_created) { + pthread_join(self->amix_thread, NULL); + self->amix_thread_created = false; + } +} + +void gsr_audio_capture_lock_filter(gsr_audio_capture *self) { + pthread_mutex_lock(&self->filter_mutex); +} + +void gsr_audio_capture_unlock_filter(gsr_audio_capture *self) { + pthread_mutex_unlock(&self->filter_mutex); +} + +static int audio_track_alloc_devices(gsr_audio_track *self, size_t num_devices) { + self->audio_devices = calloc(num_devices, sizeof(gsr_audio_device_capture)); + if(!self->audio_devices) { + gsr_log(GSR_LOG_LEVEL_ERROR, "failed to allocate audio devices"); + return GSR_ERROR_GENERIC; + } + return GSR_ERROR_OK; +} + +int gsr_audio_track_init_device_inputs(gsr_audio_track *self, const gsr_merged_audio_inputs *merged_audio_inputs, AVCodecContext *audio_codec_context, int num_channels, double num_audio_frames_shift, AVFilterContext **src_filter_ctx, bool use_amix) { + const int alloc_result = audio_track_alloc_devices(self, merged_audio_inputs->num_items); + if(alloc_result != GSR_ERROR_OK) + return alloc_result; + + for(size_t i = 0; i < merged_audio_inputs->num_items; ++i) { + const gsr_audio_input *audio_input = &merged_audio_inputs->items[i]; + gsr_audio_device_capture *device = &self->audio_devices[i]; + device->audio_input = *audio_input; + device->src_filter_ctx = use_amix ? src_filter_ctx[i] : NULL; + + if(audio_input->name[0] == '\0') { + device->sound_device.handle = NULL; + device->sound_device.frames = 0; + } else { + char description[GSR_AUDIO_INPUT_NAME_MAX_SIZE + 8]; + snprintf(description, sizeof(description), "gsr-%s", audio_input->name); + if(sound_device_get_by_name(&device->sound_device, description, audio_input->name, description, num_channels, audio_codec_context->frame_size, audio_codec_context_get_audio_format(audio_codec_context)) != 0) { + gsr_log(GSR_LOG_LEVEL_ERROR, "failed to get \"%s\" audio device", audio_input->name); + return GSR_ERROR_GENERIC; + } + } + + device->frame = create_audio_frame(audio_codec_context); + if(!device->frame) + return GSR_ERROR_GENERIC; + device->frame->pts = -audio_codec_context->frame_size * num_audio_frames_shift; + + ++self->num_audio_devices; + } + + return GSR_ERROR_OK; +} + +#ifdef GSR_APP_AUDIO +int gsr_audio_track_init_application_input(gsr_audio_track *self, const gsr_merged_audio_inputs *merged_audio_inputs, AVCodecContext *audio_codec_context, int num_channels, double num_audio_frames_shift, gsr_pipewire_audio *pipewire_audio) { + const int alloc_result = audio_track_alloc_devices(self, 1); + if(alloc_result != GSR_ERROR_OK) + return alloc_result; + + gsr_audio_device_capture *device = &self->audio_devices[0]; + device->frame = create_audio_frame(audio_codec_context); + if(!device->frame) + return GSR_ERROR_GENERIC; + device->frame->pts = -audio_codec_context->frame_size * num_audio_frames_shift; + ++self->num_audio_devices; + + char random_str[8]; + if(!generate_random_characters_standard_alphabet(random_str, sizeof(random_str))) { + gsr_log(GSR_LOG_LEVEL_ERROR, "failed to generate random string"); + return GSR_ERROR_GENERIC; + } + + char combined_sink_name[64]; + snprintf(combined_sink_name, sizeof(combined_sink_name), "gsr-combined-%.*s.monitor", (int)sizeof(random_str), random_str); + + if(sound_device_get_by_name(&device->sound_device, combined_sink_name, "", "gpu-screen-recorder", num_channels, audio_codec_context->frame_size, audio_codec_context_get_audio_format(audio_codec_context)) != 0) { + gsr_log(GSR_LOG_LEVEL_ERROR, "failed to setup audio recording to combined sink"); + return GSR_ERROR_GENERIC; + } + + const char **audio_devices_sources = calloc(merged_audio_inputs->num_items, sizeof(const char*)); + const char **app_names = calloc(merged_audio_inputs->num_items, sizeof(const char*)); + if(!audio_devices_sources || !app_names) { + free(audio_devices_sources); + free(app_names); + gsr_log(GSR_LOG_LEVEL_ERROR, "failed to allocate application audio names"); + return GSR_ERROR_GENERIC; + } + + size_t num_audio_devices_sources = 0; + size_t num_app_names = 0; + bool app_audio_inverted = false; + for(size_t i = 0; i < merged_audio_inputs->num_items; ++i) { + const gsr_audio_input *audio_input = &merged_audio_inputs->items[i]; + if(audio_input->type == GSR_AUDIO_INPUT_TYPE_DEVICE) { + audio_devices_sources[num_audio_devices_sources] = audio_input->name; + ++num_audio_devices_sources; + } else if(audio_input->type == GSR_AUDIO_INPUT_TYPE_APPLICATION) { + app_names[num_app_names] = audio_input->name; + ++num_app_names; + app_audio_inverted = audio_input->inverted; + } + } + + int result = GSR_ERROR_OK; + if(num_audio_devices_sources > 0) { + if(!gsr_pipewire_audio_add_link_from_sources_to_stream(pipewire_audio, audio_devices_sources, num_audio_devices_sources, combined_sink_name)) { + gsr_log(GSR_LOG_LEVEL_ERROR, "failed to add application audio link"); + result = GSR_ERROR_GENERIC; + } + } + + if(result == GSR_ERROR_OK) { + const bool link_added = app_audio_inverted + ? gsr_pipewire_audio_add_link_from_apps_to_stream_inverted(pipewire_audio, app_names, num_app_names, combined_sink_name) + : gsr_pipewire_audio_add_link_from_apps_to_stream(pipewire_audio, app_names, num_app_names, combined_sink_name); + if(!link_added) { + gsr_log(GSR_LOG_LEVEL_ERROR, "failed to add application audio link"); + result = GSR_ERROR_GENERIC; + } + } + + free(audio_devices_sources); + free(app_names); + return result; +} +#endif + +void gsr_audio_track_deinit(gsr_audio_track *self) { + for(size_t i = 0; i < self->num_audio_devices; ++i) { + gsr_audio_device_capture *device = &self->audio_devices[i]; + sound_device_close(&device->sound_device); + if(device->frame) + av_frame_free(&device->frame); + } + + if(self->audio_devices) { + free(self->audio_devices); + self->audio_devices = NULL; + } + self->num_audio_devices = 0; + + if(self->graph) + avfilter_graph_free(&self->graph); + + if(self->codec_context) + avcodec_free_context(&self->codec_context); +} + diff --git a/src/recorder/audio_codec.c b/src/recorder/audio_codec.c new file mode 100644 index 0000000..e8f89e7 --- /dev/null +++ b/src/recorder/audio_codec.c @@ -0,0 +1,219 @@ +#include "../../include/recorder/audio_codec.h" +#include "../../include/ffmpeg_utils.h" +#include "../../include/log.h" + +#include <assert.h> +#include <math.h> + +#include <libavutil/opt.h> + +enum AVCodecID audio_codec_get_id(gsr_audio_codec audio_codec) { + switch(audio_codec) { + case GSR_AUDIO_CODEC_AAC: return AV_CODEC_ID_AAC; + case GSR_AUDIO_CODEC_OPUS: return AV_CODEC_ID_OPUS; + case GSR_AUDIO_CODEC_FLAC: return AV_CODEC_ID_FLAC; + } + assert(false); + return AV_CODEC_ID_AAC; +} + +enum AVSampleFormat audio_codec_get_sample_format(AVCodecContext *audio_codec_context, gsr_audio_codec audio_codec, const AVCodec *codec, bool mix_audio) { + (void)audio_codec_context; + switch(audio_codec) { + case GSR_AUDIO_CODEC_AAC: { + return AV_SAMPLE_FMT_FLTP; + } + case GSR_AUDIO_CODEC_OPUS: { + bool supports_s16 = false; + bool supports_flt = false; + + #if LIBAVCODEC_VERSION_INT < AV_VERSION_INT(61, 15, 0) + for(size_t i = 0; codec->sample_fmts && codec->sample_fmts[i] != AV_SAMPLE_FMT_NONE; ++i) { + if(codec->sample_fmts[i] == AV_SAMPLE_FMT_S16) { + supports_s16 = true; + } else if(codec->sample_fmts[i] == AV_SAMPLE_FMT_FLT) { + supports_flt = true; + } + } + #else + const enum AVSampleFormat *sample_fmts = NULL; + if(avcodec_get_supported_config(audio_codec_context, codec, AV_CODEC_CONFIG_SAMPLE_FORMAT, 0, (const void**)&sample_fmts, NULL) >= 0) { + if(sample_fmts) { + for(size_t i = 0; sample_fmts[i] != AV_SAMPLE_FMT_NONE; ++i) { + if(sample_fmts[i] == AV_SAMPLE_FMT_S16) { + supports_s16 = true; + } else if(sample_fmts[i] == AV_SAMPLE_FMT_FLT) { + supports_flt = true; + } + } + } else { + // What a dumb API. It returns NULL if all formats are supported + supports_s16 = true; + supports_flt = true; + } + } + #endif + + // Amix only works with float audio + if(mix_audio) + supports_s16 = false; + + if(!supports_s16 && !supports_flt) { + gsr_log(GSR_LOG_LEVEL_WARNING, "opus audio codec is chosen but your ffmpeg version does not support s16/flt sample format and performance might be slightly worse.\n" + " You can either rebuild ffmpeg with libopus instead of the built-in opus, use the flatpak version of gpu screen recorder or record with aac audio codec instead (-ac aac).\n" + " Falling back to fltp audio sample format instead."); + } + + if(supports_s16) + return AV_SAMPLE_FMT_S16; + else if(supports_flt) + return AV_SAMPLE_FMT_FLT; + else + return AV_SAMPLE_FMT_FLTP; + } + case GSR_AUDIO_CODEC_FLAC: { + return AV_SAMPLE_FMT_S32; + } + } + assert(false); + return AV_SAMPLE_FMT_FLTP; +} + +int64_t audio_codec_get_get_bitrate(gsr_audio_codec audio_codec) { + switch(audio_codec) { + case GSR_AUDIO_CODEC_AAC: return 160000; + case GSR_AUDIO_CODEC_OPUS: return 128000; + case GSR_AUDIO_CODEC_FLAC: return 128000; + } + assert(false); + return 128000; +} + +gsr_audio_format audio_codec_context_get_audio_format(const AVCodecContext *audio_codec_context) { + switch(audio_codec_context->sample_fmt) { + case AV_SAMPLE_FMT_FLT: return GSR_AUDIO_FORMAT_F32; + case AV_SAMPLE_FMT_FLTP: return GSR_AUDIO_FORMAT_S32; + case AV_SAMPLE_FMT_S16: return GSR_AUDIO_FORMAT_S16; + case AV_SAMPLE_FMT_S32: return GSR_AUDIO_FORMAT_S32; + default: return GSR_AUDIO_FORMAT_S16; + } +} + +enum AVSampleFormat audio_format_to_sample_format(const gsr_audio_format audio_format) { + switch(audio_format) { + case GSR_AUDIO_FORMAT_S16: return AV_SAMPLE_FMT_S16; + case GSR_AUDIO_FORMAT_S32: return AV_SAMPLE_FMT_S32; + case GSR_AUDIO_FORMAT_F32: return AV_SAMPLE_FMT_FLT; + } + assert(false); + return AV_SAMPLE_FMT_S16; +} + +AVCodecContext* create_audio_codec_context(int fps, gsr_audio_codec audio_codec, bool mix_audio, int64_t audio_bitrate) { + (void)fps; + const AVCodec *codec = avcodec_find_encoder(audio_codec_get_id(audio_codec)); + if (!codec) { + gsr_log(GSR_LOG_LEVEL_ERROR, "Could not find %s audio encoder", audio_codec_get_name(audio_codec)); + return NULL; + } + + AVCodecContext *codec_context = avcodec_alloc_context3(codec); + + assert(codec->type == AVMEDIA_TYPE_AUDIO); + codec_context->codec_id = codec->id; + codec_context->sample_fmt = audio_codec_get_sample_format(codec_context, audio_codec, codec, mix_audio); + codec_context->bit_rate = audio_bitrate == 0 ? audio_codec_get_get_bitrate(audio_codec) : audio_bitrate; + codec_context->sample_rate = GSR_AUDIO_SAMPLE_RATE; + if(audio_codec == GSR_AUDIO_CODEC_AAC) { +#if LIBAVCODEC_VERSION_MAJOR < 62 + codec_context->profile = FF_PROFILE_AAC_LOW; +#else + codec_context->profile = AV_PROFILE_AAC_LOW; +#endif + } +#if LIBAVCODEC_VERSION_MAJOR < 60 + codec_context->channel_layout = AV_CH_LAYOUT_STEREO; + codec_context->channels = 2; +#else + av_channel_layout_default(&codec_context->ch_layout, 2); +#endif + + codec_context->time_base.num = 1; + codec_context->time_base.den = codec_context->sample_rate; + codec_context->thread_count = 1; + codec_context->flags |= AV_CODEC_FLAG_GLOBAL_HEADER; + + return codec_context; +} + +bool open_audio(AVCodecContext *audio_codec_context, const char *ffmpeg_audio_opts) { + AVDictionary *options = NULL; + av_dict_set(&options, "strict", "experimental", 0); + + if(ffmpeg_audio_opts) + av_dict_parse_string(&options, ffmpeg_audio_opts, "=", ";", 0); + + int ret; + ret = avcodec_open2(audio_codec_context, audio_codec_context->codec, &options); + if(ret < 0) { + gsr_log(GSR_LOG_LEVEL_ERROR, "failed to open audio codec, reason: %s", gsr_av_error_to_string(ret)); + return false; + } + + return true; +} + +AVFrame* create_audio_frame(AVCodecContext *audio_codec_context) { + AVFrame *frame = av_frame_alloc(); + if(!frame) { + gsr_log(GSR_LOG_LEVEL_ERROR, "failed to allocate audio frame"); + return NULL; + } + + frame->sample_rate = audio_codec_context->sample_rate; + frame->nb_samples = audio_codec_context->frame_size; + frame->format = audio_codec_context->sample_fmt; +#if LIBAVCODEC_VERSION_MAJOR < 60 + frame->channels = audio_codec_context->channels; + frame->channel_layout = audio_codec_context->channel_layout; +#else + av_channel_layout_copy(&frame->ch_layout, &audio_codec_context->ch_layout); +#endif + + int ret = av_frame_get_buffer(frame, 0); + if(ret < 0) { + gsr_log(GSR_LOG_LEVEL_ERROR, "failed to allocate audio data buffers, reason: %s", gsr_av_error_to_string(ret)); + av_frame_free(&frame); + return NULL; + } + + return frame; +} + +double audio_codec_get_desired_delay(gsr_audio_codec audio_codec, int fps) { + const double fps_inv = 1.0 / (double)fps; + const double base = 0.01 + 1.0/165.0; + switch(audio_codec) { + case GSR_AUDIO_CODEC_OPUS: + return fmax(0.0, base - fps_inv); + case GSR_AUDIO_CODEC_AAC: + return fmax(0.0, (base + 0.008) * 2.0 - fps_inv); + case GSR_AUDIO_CODEC_FLAC: + // TODO: Test + return fmax(0.0, base - fps_inv); + } + assert(false); + return fmax(0.0, base - fps_inv); +} + +int audio_codec_get_frame_size(gsr_audio_codec audio_codec) { + switch(audio_codec) { + case GSR_AUDIO_CODEC_AAC: return 1024; + case GSR_AUDIO_CODEC_OPUS: return 960; + case GSR_AUDIO_CODEC_FLAC: + assert(false); + return 1024; + } + assert(false); + return 1024; +} diff --git a/src/recorder/audio_input.c b/src/recorder/audio_input.c new file mode 100644 index 0000000..bbfb9bd --- /dev/null +++ b/src/recorder/audio_input.c @@ -0,0 +1,336 @@ +#include "../../include/recorder/audio_input.h" +#include "../../include/recorder/error.h" +#include "../../include/utils.h" +#include "../../include/log.h" + +#include <string.h> +#include <strings.h> +#include <stdlib.h> +#include <stdio.h> + +typedef struct { + gsr_merged_audio_inputs *merged_audio_inputs; + int error; +} parse_audio_input_userdata; + +bool gsr_app_audio_names_add(gsr_app_audio_names *self, const char *name) { + if(!gsr_array_ensure_capacity((void**)&self->items, self->num_items, &self->capacity_items, sizeof(gsr_app_audio_name))) + return false; + + snprintf(self->items[self->num_items].name, sizeof(self->items[self->num_items].name), "%s", name); + ++self->num_items; + return true; +} + +void gsr_app_audio_names_deinit(gsr_app_audio_names *self) { + if(self->items) { + free(self->items); + self->items = NULL; + } + self->num_items = 0; + self->capacity_items = 0; +} + +bool gsr_merged_audio_inputs_add(gsr_merged_audio_inputs *self, const gsr_audio_input *audio_input) { + if(!gsr_array_ensure_capacity((void**)&self->items, self->num_items, &self->capacity_items, sizeof(gsr_audio_input))) + return false; + + self->items[self->num_items] = *audio_input; + ++self->num_items; + return true; +} + +void gsr_merged_audio_inputs_deinit(gsr_merged_audio_inputs *self) { + if(self->items) { + free(self->items); + self->items = NULL; + } + self->num_items = 0; + self->capacity_items = 0; +} + +static bool parse_audio_input_callback(const char *sub, size_t size, void *userdata) { + parse_audio_input_userdata *parse_userdata = userdata; + if(size == 0) + return true; + + gsr_audio_input audio_input; + memset(&audio_input, 0, sizeof(audio_input)); + snprintf(audio_input.name, sizeof(audio_input.name), "%.*s", (int)size, sub); + + const size_t name_size = strlen(audio_input.name); + if(gsr_string_starts_with(audio_input.name, name_size, "name:")) { + snprintf(parse_userdata->merged_audio_inputs->track_name, sizeof(parse_userdata->merged_audio_inputs->track_name), "%s", audio_input.name + 5); + parse_userdata->merged_audio_inputs->has_custom_name = true; + return true; + } else if(gsr_string_starts_with(audio_input.name, name_size, "app:")) { + memmove(audio_input.name, audio_input.name + 4, name_size - 4 + 1); + audio_input.type = GSR_AUDIO_INPUT_TYPE_APPLICATION; + audio_input.inverted = false; + } else if(gsr_string_starts_with(audio_input.name, name_size, "app-inverse:")) { + memmove(audio_input.name, audio_input.name + 12, name_size - 12 + 1); + audio_input.type = GSR_AUDIO_INPUT_TYPE_APPLICATION; + audio_input.inverted = true; + } else if(gsr_string_starts_with(audio_input.name, name_size, "device:")) { + memmove(audio_input.name, audio_input.name + 7, name_size - 7 + 1); + audio_input.type = GSR_AUDIO_INPUT_TYPE_DEVICE; + } else { + audio_input.type = GSR_AUDIO_INPUT_TYPE_DEVICE; + } + + if(!gsr_merged_audio_inputs_add(parse_userdata->merged_audio_inputs, &audio_input)) { + parse_userdata->error = GSR_ERROR_GENERIC; + return false; + } + + return true; +} + +int gsr_merged_audio_inputs_parse(gsr_merged_audio_inputs *self, const char *str) { + memset(self, 0, sizeof(*self)); + + parse_audio_input_userdata userdata; + userdata.merged_audio_inputs = self; + userdata.error = GSR_ERROR_OK; + + gsr_string_split(str, '|', parse_audio_input_callback, &userdata); + if(userdata.error != GSR_ERROR_OK) { + gsr_merged_audio_inputs_deinit(self); + return userdata.error; + } + + return GSR_ERROR_OK; +} + +bool gsr_audio_input_tracks_add(gsr_audio_input_tracks *self, const gsr_merged_audio_inputs *merged_audio_inputs) { + if(!gsr_array_ensure_capacity((void**)&self->items, self->num_items, &self->capacity_items, sizeof(gsr_merged_audio_inputs))) + return false; + + self->items[self->num_items] = *merged_audio_inputs; + ++self->num_items; + return true; +} + +void gsr_audio_input_tracks_deinit(gsr_audio_input_tracks *self) { + for(size_t i = 0; i < self->num_items; ++i) { + gsr_merged_audio_inputs_deinit(&self->items[i]); + } + + if(self->items) { + free(self->items); + self->items = NULL; + } + self->num_items = 0; + self->capacity_items = 0; +} + +static const gsr_audio_device* get_audio_device_by_name(const gsr_audio_devices *audio_devices, const char *name) { + for(size_t i = 0; i < audio_devices->num_items; ++i) { + if(strcmp(audio_devices->items[i].name, name) == 0) + return &audio_devices->items[i]; + } + return NULL; +} + +static void audio_track_title_append(char *title, size_t title_size, size_t *offset, const char *str) { + const int written = snprintf(title + *offset, *offset < title_size ? title_size - *offset : 0, "%s", str); + if(written > 0) + *offset += written; +} + +/* Manually check if the audio inputs we give exist. This is only needed for pipewire, not pulseaudio. + Pipewire instead defaults to the default audio input if the audio input doesn't exist */ +static int validate_audio_inputs_get_track_name(const gsr_merged_audio_inputs *merged_audio_inputs, const gsr_audio_devices *audio_devices, char *track_name, size_t track_name_size) { + size_t offset = 0; + bool has_devices = false; + bool has_applications = false; + bool app_inverse = false; + track_name[0] = '\0'; + + for(size_t i = 0; i < merged_audio_inputs->num_items; ++i) { + const gsr_audio_input *audio_input = &merged_audio_inputs->items[i]; + if(audio_input->type == GSR_AUDIO_INPUT_TYPE_APPLICATION) + continue; + + const char *device_description = NULL; + if(strcmp(audio_input->name, "default_output") == 0) { + if(audio_devices->default_output[0] == '\0') { + gsr_log(GSR_LOG_LEVEL_ERROR, "-a default_output was specified but no default audio output is specified in the audio server"); + return GSR_ERROR_UNSUPPORTED; + } + device_description = "Default output"; + } else if(strcmp(audio_input->name, "default_input") == 0) { + if(audio_devices->default_input[0] == '\0') { + gsr_log(GSR_LOG_LEVEL_ERROR, "-a default_input was specified but no default audio input is specified in the audio server"); + return GSR_ERROR_UNSUPPORTED; + } + device_description = "Default input"; + } else { + const gsr_audio_device *audio_device = get_audio_device_by_name(audio_devices, audio_input->name); + if(audio_device) + device_description = audio_device->description; + } + + if(!device_description) { + gsr_log(GSR_LOG_LEVEL_ERROR, "Audio device '%s' is not a valid audio device, expected one of:", audio_input->name); + if(audio_devices->default_output[0] != '\0') + fprintf(stderr, " default_output (Default output)\n"); + if(audio_devices->default_input[0] != '\0') + fprintf(stderr, " default_input (Default input)\n"); + for(size_t j = 0; j < audio_devices->num_items; ++j) { + fprintf(stderr, " %s (%s)\n", audio_devices->items[j].name, audio_devices->items[j].description); + } + return GSR_ERROR_AUDIO_DEVICE_NOT_FOUND; + } + + audio_track_title_append(track_name, track_name_size, &offset, has_devices ? ", " : "Devices: "); + audio_track_title_append(track_name, track_name_size, &offset, device_description); + has_devices = true; + } + + for(size_t i = 0; i < merged_audio_inputs->num_items; ++i) { + const gsr_audio_input *audio_input = &merged_audio_inputs->items[i]; + if(audio_input->type != GSR_AUDIO_INPUT_TYPE_APPLICATION) + continue; + + app_inverse = audio_input->inverted; + if(!has_applications) { + if(has_devices) + audio_track_title_append(track_name, track_name_size, &offset, ". "); + audio_track_title_append(track_name, track_name_size, &offset, app_inverse ? "All applications except: " : "Applications: "); + } else { + audio_track_title_append(track_name, track_name_size, &offset, ", "); + } + + audio_track_title_append(track_name, track_name_size, &offset, audio_input->name); + has_applications = true; + } + + return GSR_ERROR_OK; +} + +int gsr_audio_input_tracks_parse(gsr_audio_input_tracks *self, const char **audio_input_args, int num_audio_input_args, const gsr_audio_devices *audio_devices) { + memset(self, 0, sizeof(*self)); + + for(int i = 0; i < num_audio_input_args; ++i) { + const char *audio_input = audio_input_args[i]; + if(!audio_input || audio_input[0] == '\0') + continue; + + gsr_merged_audio_inputs merged_audio_inputs; + const int parse_result = gsr_merged_audio_inputs_parse(&merged_audio_inputs, audio_input); + if(parse_result != GSR_ERROR_OK) { + gsr_audio_input_tracks_deinit(self); + return parse_result; + } + + char track_name[GSR_AUDIO_TRACK_NAME_MAX_SIZE]; + const int validate_result = validate_audio_inputs_get_track_name(&merged_audio_inputs, audio_devices, track_name, sizeof(track_name)); + if(validate_result != GSR_ERROR_OK) { + gsr_merged_audio_inputs_deinit(&merged_audio_inputs); + gsr_audio_input_tracks_deinit(self); + return validate_result; + } + + if(!merged_audio_inputs.has_custom_name) + snprintf(merged_audio_inputs.track_name, sizeof(merged_audio_inputs.track_name), "%s", track_name); + + if(!gsr_audio_input_tracks_add(self, &merged_audio_inputs)) { + gsr_merged_audio_inputs_deinit(&merged_audio_inputs); + gsr_audio_input_tracks_deinit(self); + return GSR_ERROR_GENERIC; + } + } + + return GSR_ERROR_OK; +} + +bool gsr_audio_inputs_has_app_audio(const gsr_merged_audio_inputs *self) { + for(size_t i = 0; i < self->num_items; ++i) { + if(self->items[i].type == GSR_AUDIO_INPUT_TYPE_APPLICATION) + return true; + } + return false; +} + +/* Should use amix if more than 1 audio device and 0 application audio, merged */ +bool gsr_audio_inputs_should_use_amix(const gsr_merged_audio_inputs *self) { + int num_audio_devices = 0; + int num_app_audio = 0; + + for(size_t i = 0; i < self->num_items; ++i) { + if(self->items[i].type == GSR_AUDIO_INPUT_TYPE_DEVICE) + ++num_audio_devices; + else if(self->items[i].type == GSR_AUDIO_INPUT_TYPE_APPLICATION) + ++num_app_audio; + } + + return num_audio_devices > 1 && num_app_audio == 0; +} + +bool gsr_audio_input_tracks_has_app_audio(const gsr_audio_input_tracks *self) { + for(size_t i = 0; i < self->num_items; ++i) { + if(gsr_audio_inputs_has_app_audio(&self->items[i])) + return true; + } + return false; +} + +bool gsr_audio_input_tracks_should_use_amix(const gsr_audio_input_tracks *self) { + for(size_t i = 0; i < self->num_items; ++i) { + if(gsr_audio_inputs_should_use_amix(&self->items[i])) + return true; + } + return false; +} + +static void match_app_audio_input_to_available_apps(const gsr_merged_audio_inputs *merged_audio_inputs, const gsr_app_audio_names *app_audio_names) { + for(size_t i = 0; i < merged_audio_inputs->num_items; ++i) { + const gsr_audio_input *audio_input = &merged_audio_inputs->items[i]; + if(audio_input->type != GSR_AUDIO_INPUT_TYPE_APPLICATION || audio_input->inverted) + continue; + + bool match = false; + for(size_t j = 0; j < app_audio_names->num_items; ++j) { + if(strcasecmp(app_audio_names->items[j].name, audio_input->name) == 0) { + match = true; + break; + } + } + + if(!match) { + gsr_log(GSR_LOG_LEVEL_WARNING, "no audio application with the name \"%s\" was found, expected one of the following:", audio_input->name); + for(size_t j = 0; j < app_audio_names->num_items; ++j) { + fprintf(stderr, " * %s\n", app_audio_names->items[j].name); + } + fprintf(stderr, " assuming this is intentional (if you are trying to record audio for applications that haven't started yet).\n"); + } + } +} + +int gsr_audio_input_tracks_validate_app_audio(const gsr_audio_input_tracks *self, const gsr_app_audio_names *app_audio_names) { + for(size_t i = 0; i < self->num_items; ++i) { + const gsr_merged_audio_inputs *merged_audio_inputs = &self->items[i]; + int num_app_audio = 0; + int num_app_inverted_audio = 0; + + for(size_t j = 0; j < merged_audio_inputs->num_items; ++j) { + const gsr_audio_input *audio_input = &merged_audio_inputs->items[j]; + if(audio_input->type == GSR_AUDIO_INPUT_TYPE_APPLICATION) { + if(audio_input->inverted) + ++num_app_inverted_audio; + else + ++num_app_audio; + } + } + + match_app_audio_input_to_available_apps(merged_audio_inputs, app_audio_names); + + if(num_app_audio > 0 && num_app_inverted_audio > 0) { + gsr_log(GSR_LOG_LEVEL_ERROR, "argument -a was provided with both app: and app-inverse:, only one of them can be used for one audio track"); + return GSR_ERROR_UNSUPPORTED; + } + } + + return GSR_ERROR_OK; +} diff --git a/src/recorder/capture_setup.c b/src/recorder/capture_setup.c new file mode 100644 index 0000000..9fced21 --- /dev/null +++ b/src/recorder/capture_setup.c @@ -0,0 +1,571 @@ +#include "../../include/recorder/capture_setup.h" +#include "../../include/recorder/error.h" +#include "../../include/recorder/windowing.h" +#include "../../include/capture/nvfbc.h" +#include "../../include/capture/xcomposite.h" +#include "../../include/capture/ximage.h" +#include "../../include/capture/kms.h" +#include "../../include/capture/v4l2.h" +#ifdef GSR_PORTAL +#include "../../include/capture/portal.h" +#endif +#include "../../include/args_parser.h" +#include "../../include/utils.h" +#include "../../include/window/window.h" +#include "../../include/log.h" + +#include <string.h> +#include <stdlib.h> +#include <stdio.h> +#include <unistd.h> + +typedef struct { + gsr_window *window; +} monitor_output_callback_userdata; + +typedef struct { + char *output_name; +} first_output_callback_userdata; + +typedef struct { + gsr_window *window; + vec2i position; + char *output_name; + vec2i monitor_pos; + vec2i monitor_size; + double monitor_scale_inverted; +} monitor_by_position_callback_userdata; + +static void monitor_output_callback_print(const gsr_monitor *monitor, void *userdata) { + const monitor_output_callback_userdata *options = userdata; + vec2i monitor_position = monitor->pos; + vec2i monitor_size = monitor->size; + if(gsr_window_get_display_server(options->window) == GSR_DISPLAY_SERVER_WAYLAND) { + gsr_monitor_rotation monitor_rotation = GSR_MONITOR_ROT_0; + drm_monitor_get_display_server_data(options->window, monitor, &monitor_rotation, &monitor_position); + if(monitor_rotation == GSR_MONITOR_ROT_90 || monitor_rotation == GSR_MONITOR_ROT_270) { + const int tmp = monitor_size.x; + monitor_size.x = monitor_size.y; + monitor_size.y = tmp; + } + } + fprintf(stderr, " \"%.*s\" (%dx%d+%d+%d)\n", monitor->name_len, monitor->name, monitor_size.x, monitor_size.y, monitor_position.x, monitor_position.y); +} + +static void monitor_output_callback_print_region(const gsr_monitor *monitor, void *userdata) { + (void)userdata; + const vec2i monitor_position = monitor->logical_pos; + const vec2i monitor_size = monitor->logical_size; + fprintf(stderr, " \"%.*s\" (%dx%d+%d+%d)\n", monitor->name_len, monitor->name, monitor_size.x, monitor_size.y, monitor_position.x, monitor_position.y); +} + +static void get_first_output_callback(const gsr_monitor *monitor, void *userdata) { + first_output_callback_userdata *data = userdata; + if(!data->output_name) + data->output_name = strdup(monitor->name); +} + +static void get_monitor_by_position_callback(const gsr_monitor *monitor, void *userdata) { + monitor_by_position_callback_userdata *data = userdata; + + const vec2i monitor_position = monitor->logical_pos; + const vec2i monitor_size = monitor->size; + const vec2i monitor_logical_size = monitor->logical_size; + + if(!data->output_name && data->position.x >= monitor_position.x && data->position.x <= monitor_position.x + monitor_logical_size.x + && data->position.y >= monitor_position.y && data->position.y <= monitor_position.y + monitor_logical_size.y) + { + data->output_name = strdup(monitor->name); + data->monitor_pos = monitor_position; + data->monitor_size = monitor_size; + data->monitor_scale_inverted = (double)monitor_size.x / (double)monitor_logical_size.x; + } +} + +void gsr_capture_deps_init(gsr_capture_deps *self) { + memset(self, 0, sizeof(*self)); +} + +void gsr_capture_deps_init_cursor(gsr_capture_deps *self, gsr_egl *egl, bool record_cursor) { + if(gsr_window_get_display_server(egl->window) != GSR_DISPLAY_SERVER_X11 || !record_cursor) + return; + + self->x11_cursor_display = (Display*)gsr_window_get_display(egl->window); + gsr_cursor_init(&self->x11_cursor, egl, self->x11_cursor_display); +} + +void gsr_capture_deps_deinit(gsr_capture_deps *self) { + gsr_cursor_deinit(&self->x11_cursor); + self->x11_cursor_display = NULL; + + if(self->kms_client_initialized) { + gsr_capture_deps_cleanup_kms_fds(self); + gsr_kms_client_deinit(&self->kms_client); + self->kms_client_initialized = false; + } + + gsr_kde_night_light_destroy(self->kde_night_light); + self->kde_night_light = NULL; + self->kde_night_light_initialized = false; +} + +void gsr_capture_deps_cleanup_kms_fds(gsr_capture_deps *self) { + for(int i = 0; i < self->kms_response.num_items; ++i) { + for(int j = 0; j < self->kms_response.items[i].num_dma_bufs; ++j) { + gsr_kms_response_dma_buf *dma_buf = &self->kms_response.items[i].dma_buf[j]; + if(dma_buf->fd > 0) { + close(dma_buf->fd); + dma_buf->fd = 0; + } + } + self->kms_response.items[i].num_dma_bufs = 0; + } + self->kms_response.num_items = 0; +} + +void gsr_capture_deps_update_kms(gsr_capture_deps *self) { + if(!self->kms_client_initialized) + return; + + if(gsr_kms_client_get_kms(&self->kms_client, &self->kms_response) != 0) + gsr_log(GSR_LOG_LEVEL_ERROR, "failed to get kms, error: %d (%s)", self->kms_response.result, self->kms_response.err_msg); +} + +static int validate_monitor_get_valid(const gsr_egl *egl, const char *window, char *output_name, size_t output_name_size) { + const bool is_x11 = gsr_window_get_display_server(egl->window) == GSR_DISPLAY_SERVER_X11; + const gsr_connection_type connection_type = is_x11 ? GSR_CONNECTION_X11 : GSR_CONNECTION_DRM; + const bool capture_use_drm = monitor_capture_use_drm(egl->window, egl->gpu_info.vendor); + + snprintf(output_name, output_name_size, "%s", window); + if(strcmp(output_name, "screen") == 0) { + first_output_callback_userdata data; + data.output_name = NULL; + for_each_active_monitor_output(egl->window, egl->card_path, connection_type, get_first_output_callback, &data); + + if(data.output_name) { + snprintf(output_name, output_name_size, "%s", data.output_name); + free(data.output_name); + } else { + gsr_log(GSR_LOG_LEVEL_ERROR, "no usable output found"); + return GSR_ERROR_MONITOR_NOT_FOUND; + } + } else if(capture_use_drm || (strcmp(output_name, "screen-direct") != 0 && strcmp(output_name, "screen-direct-force") != 0)) { + gsr_monitor gmon; + if(!get_monitor_by_name(egl, connection_type, output_name, &gmon)) { + gsr_log(GSR_LOG_LEVEL_ERROR, "display \"%s\" not found, expected one of:", output_name); + fprintf(stderr, " \"screen\"\n"); + if(!capture_use_drm) + fprintf(stderr, " \"screen-direct\"\n"); + + monitor_output_callback_userdata userdata; + userdata.window = egl->window; + for_each_active_monitor_output(egl->window, egl->card_path, connection_type, monitor_output_callback_print, &userdata); + return GSR_ERROR_MONITOR_NOT_FOUND; + } + } + + return GSR_ERROR_OK; +} + +static bool get_monitor_by_region_center(const gsr_egl *egl, vec2i region_position, vec2i region_size, char *output_name, size_t output_name_size, vec2i *monitor_pos, vec2i *monitor_size, double *monitor_scale_inverted) { + const bool is_x11 = gsr_window_get_display_server(egl->window) == GSR_DISPLAY_SERVER_X11; + const gsr_connection_type connection_type = is_x11 ? GSR_CONNECTION_X11 : GSR_CONNECTION_WAYLAND; + + monitor_by_position_callback_userdata data; + data.window = egl->window; + data.position = (vec2i){ region_position.x + region_size.x / 2, region_position.y + region_size.y / 2 }; + data.output_name = NULL; + data.monitor_pos = (vec2i){0, 0}; + data.monitor_size = (vec2i){0, 0}; + data.monitor_scale_inverted = 1.0; + for_each_active_monitor_output(egl->window, egl->card_path, connection_type, get_monitor_by_position_callback, &data); + + output_name[0] = '\0'; + if(data.output_name) { + snprintf(output_name, output_name_size, "%s", data.output_name); + free(data.output_name); + } + *monitor_pos = data.monitor_pos; + *monitor_size = data.monitor_size; + *monitor_scale_inverted = data.monitor_scale_inverted; + return output_name[0] != '\0'; +} + +static int region_get_data(gsr_egl *egl, vec2i *region_size, vec2i *region_position, char *output_name, size_t output_name_size) { + vec2i monitor_pos = {0, 0}; + vec2i monitor_size = {0, 0}; + double monitor_scale_inverted = 1.0; + if(!get_monitor_by_region_center(egl, *region_position, *region_size, output_name, output_name_size, &monitor_pos, &monitor_size, &monitor_scale_inverted)) { + const bool is_x11 = gsr_window_get_display_server(egl->window) == GSR_DISPLAY_SERVER_X11; + const gsr_connection_type connection_type = is_x11 ? GSR_CONNECTION_X11 : GSR_CONNECTION_WAYLAND; + gsr_log(GSR_LOG_LEVEL_ERROR, "the region %dx%d+%d+%d doesn't match any monitor. Available monitors and their regions:", region_size->x, region_size->y, region_position->x, region_position->y); + for_each_active_monitor_output(egl->window, egl->card_path, connection_type, monitor_output_callback_print_region, NULL); + return GSR_ERROR_MONITOR_NOT_FOUND; + } + + /* Capture whole monitor when region size is set to 0x0 */ + if(region_size->x == 0 && region_size->y == 0) { + region_position->x = 0; + region_position->y = 0; + } else { + region_position->x -= monitor_pos.x; + region_position->y -= monitor_pos.y; + /* Match drm plane coordinate space (1x scaling) to wayland coordinate space (which may have scaling set by user) */ + region_position->x *= monitor_scale_inverted; + region_position->y *= monitor_scale_inverted; + + region_size->x *= monitor_scale_inverted; + region_size->y *= monitor_scale_inverted; + } + + return GSR_ERROR_OK; +} + +static gsr_capture* create_monitor_capture(const gsr_recorder_settings *settings, gsr_egl *egl, gsr_capture_deps *deps, const gsr_capture_source *capture_source, bool prefer_ximage, int *error) { + *error = GSR_ERROR_OK; + + if(gsr_window_get_display_server(egl->window) == GSR_DISPLAY_SERVER_X11 && prefer_ximage) { + gsr_capture_ximage_params ximage_params; + memset(&ximage_params, 0, sizeof(ximage_params)); + ximage_params.egl = egl; + ximage_params.cursor = &deps->x11_cursor; + ximage_params.display_to_capture = capture_source->name; + ximage_params.record_cursor = settings->record_cursor; + ximage_params.output_resolution = settings->output_resolution; + ximage_params.region_size = capture_source->region_size; + ximage_params.region_position = capture_source->region_pos; + return gsr_capture_ximage_create(&ximage_params); + } + + if(monitor_capture_use_drm(egl->window, egl->gpu_info.vendor)) { + if(!deps->kms_client_initialized) { + deps->kms_client_initialized = true; + const int kms_init_res = gsr_kms_client_init(&deps->kms_client, egl->card_path); + if(kms_init_res != 0) { + *error = kms_init_res < 0 ? GSR_ERROR_GENERIC : -kms_init_res; + return NULL; + } + } + + if(!deps->kde_night_light_initialized && gsr_window_get_display_server(egl->window) == GSR_DISPLAY_SERVER_WAYLAND) { + deps->kde_night_light_initialized = true; + deps->kde_night_light = gsr_kde_night_light_create(); + } + + gsr_capture_kms_params kms_params; + memset(&kms_params, 0, sizeof(kms_params)); + kms_params.egl = egl; + kms_params.x11_cursor = &deps->x11_cursor; + kms_params.kms_response = &deps->kms_response; + kms_params.kde_night_light = deps->kde_night_light; + kms_params.display_to_capture = capture_source->name; + kms_params.record_cursor = settings->record_cursor; + kms_params.hdr = video_codec_is_hdr(settings->video_codec); + kms_params.fps = settings->fps; + kms_params.output_resolution = settings->output_resolution; + kms_params.region_size = capture_source->region_size; + kms_params.region_position = capture_source->region_pos; + return gsr_capture_kms_create(&kms_params); + } else { + const char *capture_source_real = capture_source->name; + const bool direct_capture = strcmp(capture_source->name, "screen-direct") == 0 || strcmp(capture_source->name, "screen-direct-force") == 0; + if(direct_capture) { + capture_source_real = "screen"; + gsr_log(GSR_LOG_LEVEL_WARNING, "%s capture option is not recommended unless you use G-SYNC as Nvidia has driver issues that can cause your system or games to freeze/crash.", capture_source->name); + } + + gsr_capture_nvfbc_params nvfbc_params; + memset(&nvfbc_params, 0, sizeof(nvfbc_params)); + nvfbc_params.egl = egl; + nvfbc_params.display_to_capture = capture_source_real; + nvfbc_params.fps = settings->fps; + nvfbc_params.direct_capture = direct_capture; + nvfbc_params.record_cursor = settings->record_cursor; + nvfbc_params.output_resolution = settings->output_resolution; + nvfbc_params.region_size = capture_source->region_size; + nvfbc_params.region_position = capture_source->region_pos; + return gsr_capture_nvfbc_create(&nvfbc_params); + } +} + +static gsr_capture* create_capture_impl(const gsr_recorder_settings *settings, gsr_egl *egl, gsr_capture_deps *deps, gsr_capture_source *capture_source, bool prefer_ximage, int *error) { + bool follow_focused = false; + const bool wayland = gsr_window_get_display_server(egl->window) == GSR_DISPLAY_SERVER_WAYLAND; + + *error = GSR_ERROR_OK; + gsr_capture *capture = NULL; + if(capture_source->type == GSR_CAPTURE_SOURCE_TYPE_FOCUSED_WINDOW) { + if(wayland) { + gsr_log(GSR_LOG_LEVEL_ERROR, "GPU Screen Recorder window capture only works in a pure X11 session. Xwayland is not supported. You can record a monitor instead on wayland"); + *error = GSR_ERROR_UNSUPPORTED; + return NULL; + } + + if(settings->output_resolution.x <= 0 || settings->output_resolution.y <= 0) { + gsr_log(GSR_LOG_LEVEL_ERROR, "invalid value for option -s '%dx%d' when using -w focused option. expected width and height to be greater than 0", settings->output_resolution.x, settings->output_resolution.y); + args_parser_print_usage(); + *error = GSR_ERROR_GENERIC; + return NULL; + } + + follow_focused = true; + } else if(capture_source->type == GSR_CAPTURE_SOURCE_TYPE_PORTAL) { +#ifdef GSR_PORTAL + /* Desktop portal capture on x11 doesn't seem to be hardware accelerated */ + if(!wayland) { + gsr_log(GSR_LOG_LEVEL_ERROR, "desktop portal capture is not supported on X11"); + *error = GSR_ERROR_GENERIC; + return NULL; + } + + gsr_capture_portal_params portal_params; + memset(&portal_params, 0, sizeof(portal_params)); + portal_params.egl = egl; + portal_params.record_cursor = settings->record_cursor; + portal_params.restore_portal_session = settings->restore_portal_session; + portal_params.portal_session_token_filepath = settings->portal_session_token_filepath; + portal_params.output_resolution = settings->output_resolution; + capture = gsr_capture_portal_create(&portal_params); + if(!capture) { + *error = GSR_ERROR_GENERIC; + return NULL; + } +#else + gsr_log(GSR_LOG_LEVEL_ERROR, "option '-w portal' used but GPU Screen Recorder was compiled without desktop portal support. Please recompile GPU Screen recorder with the -Dportal=true option"); + *error = GSR_ERROR_UNSUPPORTED; + return NULL; +#endif + } else if(capture_source->type == GSR_CAPTURE_SOURCE_TYPE_REGION) { + const int region_result = region_get_data(egl, &capture_source->region_size, &capture_source->region_pos, capture_source->name, sizeof(capture_source->name)); + if(region_result != GSR_ERROR_OK) { + *error = region_result; + return NULL; + } + + capture = create_monitor_capture(settings, egl, deps, capture_source, prefer_ximage, error); + if(!capture) { + if(*error == GSR_ERROR_OK) + *error = GSR_ERROR_GENERIC; + return NULL; + } + } else if(capture_source->type == GSR_CAPTURE_SOURCE_TYPE_MONITOR) { + char monitor_name[GSR_CAPTURE_SOURCE_NAME_MAX_SIZE]; + const int monitor_result = validate_monitor_get_valid(egl, capture_source->name, monitor_name, sizeof(monitor_name)); + if(monitor_result != GSR_ERROR_OK) { + *error = monitor_result; + return NULL; + } + snprintf(capture_source->name, sizeof(capture_source->name), "%s", monitor_name); + + capture = create_monitor_capture(settings, egl, deps, capture_source, prefer_ximage, error); + if(!capture) { + if(*error == GSR_ERROR_OK) + *error = GSR_ERROR_GENERIC; + return NULL; + } + } else if(capture_source->type == GSR_CAPTURE_SOURCE_TYPE_V4L2) { + gsr_capture_v4l2_params v4l2_params; + memset(&v4l2_params, 0, sizeof(v4l2_params)); + v4l2_params.egl = egl; + v4l2_params.output_resolution = settings->output_resolution; + v4l2_params.device_path = capture_source->name; + v4l2_params.pixfmt = capture_source->v4l2_pixfmt; + v4l2_params.camera_fps = capture_source->camera_fps; + v4l2_params.camera_resolution.width = capture_source->camera_resolution.x; + v4l2_params.camera_resolution.height = capture_source->camera_resolution.y; + capture = gsr_capture_v4l2_create(&v4l2_params); + if(!capture) { + *error = GSR_ERROR_GENERIC; + return NULL; + } + } else { + if(wayland) { + gsr_log(GSR_LOG_LEVEL_ERROR, "GPU Screen Recorder window capture only works in a pure X11 session. Xwayland is not supported. You can record a monitor instead on wayland or use -w portal option which supports window capture if your wayland compositor supports window capture"); + *error = GSR_ERROR_UNSUPPORTED; + return NULL; + } + } + + if(!capture) { + gsr_capture_xcomposite_params xcomposite_params; + memset(&xcomposite_params, 0, sizeof(xcomposite_params)); + xcomposite_params.egl = egl; + xcomposite_params.cursor = &deps->x11_cursor; + xcomposite_params.window = capture_source->window_id; + xcomposite_params.follow_focused = follow_focused; + xcomposite_params.record_cursor = settings->record_cursor; + xcomposite_params.output_resolution = settings->output_resolution; + capture = gsr_capture_xcomposite_create(&xcomposite_params); + if(!capture) { + *error = GSR_ERROR_GENERIC; + return NULL; + } + } + + return capture; +} + +/* The size and position of a capture source can be relative to the video size, in which case it can't be used to calculate the video size */ +static bool video_source_size_is_relative_to_video_size(const gsr_video_source *self) { + return self->capture_source->pos.x_type == VVEC2I_TYPE_SCALAR || self->capture_source->pos.y_type == VVEC2I_TYPE_SCALAR + || (self->capture_source->size.x_type == VVEC2I_TYPE_SCALAR && self->capture_source->size.x != 100) + || (self->capture_source->size.y_type == VVEC2I_TYPE_SCALAR && self->capture_source->size.y != 100); +} + +/* The video size is the area that all capture sources cover */ +static vec2i video_sources_get_total_size(const gsr_video_sources *self) { + vec2i start_pos = {99999, 99999}; + vec2i end_pos = {-99999, -99999}; + for(size_t i = 0; i < self->num_items; ++i) { + const gsr_video_source *video_source = &self->items[i]; + if(video_source_size_is_relative_to_video_size(video_source)) + continue; + + const vec2i video_source_start_pos = { + video_source->capture_source->pos.x, + video_source->capture_source->pos.y + }; + + const vec2i video_source_end_pos = { + video_source_start_pos.x + video_source->metadata.video_size.x, + video_source_start_pos.y + video_source->metadata.video_size.y + }; + + if(video_source_start_pos.x < start_pos.x) + start_pos.x = video_source_start_pos.x; + if(video_source_start_pos.y < start_pos.y) + start_pos.y = video_source_start_pos.y; + + if(video_source_end_pos.x > end_pos.x) + end_pos.x = video_source_end_pos.x; + if(video_source_end_pos.y > end_pos.y) + end_pos.y = video_source_end_pos.y; + } + + /* Every capture source is relative to the video size, so use the size of the capture sources themselves as the video size */ + if(end_pos.x <= start_pos.x || end_pos.y <= start_pos.y) { + start_pos = (vec2i){0, 0}; + end_pos = (vec2i){0, 0}; + for(size_t i = 0; i < self->num_items; ++i) { + const vec2i capture_size = self->items[i].metadata.video_size; + if(capture_size.x > end_pos.x) + end_pos.x = capture_size.x; + if(capture_size.y > end_pos.y) + end_pos.y = capture_size.y; + } + } + + vec2i video_size = { end_pos.x - start_pos.x, end_pos.y - start_pos.y }; + if(video_size.x < 0) + video_size.x = 0; + if(video_size.y < 0) + video_size.y = 0; + + return video_size; +} + +int gsr_video_sources_create(gsr_video_sources *self, const gsr_recorder_settings *settings, gsr_egl *egl, gsr_capture_deps *deps, bool prefer_ximage, gsr_capture_sources *capture_sources, vec2i *video_size) { + memset(self, 0, sizeof(*self)); + if(capture_sources->num_items == 0) + return GSR_ERROR_GENERIC; + + self->items = calloc(capture_sources->num_items, sizeof(gsr_video_source)); + if(!self->items) { + gsr_log(GSR_LOG_LEVEL_ERROR, "failed to allocate video sources"); + return GSR_ERROR_GENERIC; + } + + for(size_t i = 0; i < capture_sources->num_items; ++i) { + gsr_capture_source *capture_source = &capture_sources->items[i]; + gsr_video_source *video_source = &self->items[i]; + + memset(&video_source->metadata, 0, sizeof(video_source->metadata)); + video_source->metadata.fps = settings->fps; + video_source->metadata.halign = capture_source->halign; + video_source->metadata.valign = capture_source->valign; + video_source->metadata.flip = (gsr_flip)capture_source->flip; + video_source->capture_source = capture_source; + + int error = GSR_ERROR_OK; + video_source->capture = create_capture_impl(settings, egl, deps, capture_source, prefer_ximage, &error); + if(!video_source->capture) { + gsr_video_sources_deinit(self); + return error; + } + + ++self->num_items; + } + + for(size_t i = 0; i < self->num_items; ++i) { + const int capture_result = gsr_capture_start(self->items[i].capture, &self->items[i].metadata); + if(capture_result != 0) { + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_capture_start failed"); + gsr_video_sources_deinit(self); + return -capture_result; + } + } + + *video_size = video_sources_get_total_size(self); + + for(size_t i = 0; i < self->num_items; ++i) { + self->items[i].metadata.video_size = *video_size; + } + + return GSR_ERROR_OK; +} + +void gsr_video_sources_update_with_real_video_size(gsr_video_sources *self, vec2i video_size) { + for(size_t i = 0; i < self->num_items; ++i) { + gsr_video_source *video_source = &self->items[i]; + const gsr_capture_source *capture_source = video_source->capture_source; + + video_source->metadata.recording_size = video_source->metadata.video_size; + /* TODO: What if this updated resolution is above max resolution? */ + video_source->metadata.video_size = video_size; + + if(capture_source->pos.x != 0 || capture_source->pos.y != 0) { + video_source->metadata.position.x = capture_source->pos.x; + video_source->metadata.position.y = capture_source->pos.y; + + if(capture_source->pos.x_type == VVEC2I_TYPE_SCALAR) + video_source->metadata.position.x = video_source->metadata.video_size.x * ((double)video_source->metadata.position.x / 100.0); + + if(capture_source->pos.y_type == VVEC2I_TYPE_SCALAR) + video_source->metadata.position.y = video_source->metadata.video_size.y * ((double)video_source->metadata.position.y / 100.0); + } + + if(capture_source->size.x != 0 || capture_source->size.y != 0) { + video_source->metadata.recording_size.x = capture_source->size.x; + video_source->metadata.recording_size.y = capture_source->size.y; + + if(capture_source->size.x_type == VVEC2I_TYPE_SCALAR) + video_source->metadata.recording_size.x = video_source->metadata.video_size.x * ((double)video_source->metadata.recording_size.x / 100.0); + + if(capture_source->size.y_type == VVEC2I_TYPE_SCALAR) + video_source->metadata.recording_size.y = video_source->metadata.video_size.y * ((double)video_source->metadata.recording_size.y / 100.0); + } + } +} + +bool gsr_video_sources_uses_external_image(const gsr_video_sources *self) { + for(size_t i = 0; i < self->num_items; ++i) { + if(gsr_capture_uses_external_image(self->items[i].capture)) + return true; + } + return false; +} + +void gsr_video_sources_deinit(gsr_video_sources *self) { + for(size_t i = 0; i < self->num_items; ++i) { + if(self->items[i].capture) { + gsr_capture_destroy(self->items[i].capture); + self->items[i].capture = NULL; + } + } + + if(self->items) { + free(self->items); + self->items = NULL; + } + self->num_items = 0; +} diff --git a/src/recorder/capture_source.c b/src/recorder/capture_source.c new file mode 100644 index 0000000..5407abc --- /dev/null +++ b/src/recorder/capture_source.c @@ -0,0 +1,390 @@ +#include "../../include/recorder/capture_source.h" +#include "../../include/recorder/error.h" +#include "../../include/utils.h" +#include "../../include/log.h" + +#include <string.h> +#include <stdlib.h> +#include <stdio.h> + +typedef struct { + gsr_capture_source *capture_source; + bool is_first_column; + int error; +} parse_capture_source_options_userdata; + +typedef struct { + gsr_capture_sources *capture_sources; + vec2i region_position; + vec2i region_size; + bool has_multiple_capture_sources; + int error; +} parse_capture_source_arg_userdata; + +void gsr_capture_source_init(gsr_capture_source *self, vec2i region_position, vec2i region_size) { + memset(self, 0, sizeof(*self)); + self->type = GSR_CAPTURE_SOURCE_TYPE_WINDOW; + self->halign = GSR_CAPTURE_ALIGN_CENTER; + self->valign = GSR_CAPTURE_ALIGN_CENTER; + self->v4l2_pixfmt = GSR_CAPTURE_V4L2_PIXFMT_AUTO; + self->flip = GSR_FLIP_NONE; + self->pos = (vvec2i){0, 0, VVEC2I_TYPE_PIXELS, VVEC2I_TYPE_PIXELS}; + self->size = (vvec2i){100, 100, VVEC2I_TYPE_SCALAR, VVEC2I_TYPE_SCALAR}; + self->region_pos = region_position; + self->region_size = region_size; +} + +static bool is_hex_num(char c) { + return (c >= 'A' && c <= 'F') || (c >= 'a' && c <= 'f') || (c >= '0' && c <= '9'); +} + +static bool contains_non_hex_number(const char *str) { + bool hex_start = false; + size_t len = strlen(str); + if(len >= 2 && memcmp(str, "0x", 2) == 0) { + str += 2; + len -= 2; + hex_start = true; + } + + bool is_hex = false; + for(size_t i = 0; i < len; ++i) { + char c = str[i]; + if(c == '\0') + return false; + if(!is_hex_num(c)) + return true; + if((c >= 'A' && c <= 'F') || (c >= 'a' && c <= 'f')) + is_hex = true; + } + + return is_hex && !hex_start; +} + +static void capture_source_type_from_string(const char *capture_source_str, size_t size, gsr_capture_source *capture_source) { + char capture_source_str_n[64]; + snprintf(capture_source_str_n, sizeof(capture_source_str_n), "%.*s", (int)size, capture_source_str); + + if(size == 7 && memcmp(capture_source_str_n, "focused", 7) == 0) { + capture_source->type = GSR_CAPTURE_SOURCE_TYPE_FOCUSED_WINDOW; + } else if(size == 6 && memcmp(capture_source_str_n, "portal", 6) == 0) { + capture_source->type = GSR_CAPTURE_SOURCE_TYPE_PORTAL; + } else if(size == 6 && memcmp(capture_source_str_n, "region", 6) == 0) { + capture_source->type = GSR_CAPTURE_SOURCE_TYPE_REGION; + } else if(size >= 10 && memcmp(capture_source_str_n, "/dev/video", 10) == 0) { + capture_source->type = GSR_CAPTURE_SOURCE_TYPE_V4L2; + } else if(sscanf(capture_source_str_n, "%dx%d+%d+%d", &capture_source->region_size.x, &capture_source->region_size.y, &capture_source->region_pos.x, &capture_source->region_pos.y) == 4) { + capture_source->type = GSR_CAPTURE_SOURCE_TYPE_REGION; + capture_source->region_set = true; + } else if(contains_non_hex_number(capture_source_str_n)) { + capture_source->type = GSR_CAPTURE_SOURCE_TYPE_MONITOR; + } else { + capture_source->type = GSR_CAPTURE_SOURCE_TYPE_WINDOW; + } +} + +static bool string_to_capture_alignment(const char *str, size_t len, gsr_capture_alignment *alignment) { + if(len == 5 && memcmp(str, "start", 5) == 0) { + *alignment = GSR_CAPTURE_ALIGN_START; + return true; + } else if(len == 6 && memcmp(str, "center", 6) == 0) { + *alignment = GSR_CAPTURE_ALIGN_CENTER; + return true; + } else if(len == 3 && memcmp(str, "end", 3) == 0) { + *alignment = GSR_CAPTURE_ALIGN_END; + return true; + } else { + return false; + } +} + +static bool string_to_v4l2_pixfmt(const char *str, size_t len, gsr_capture_v4l2_pixfmt *pixfmt) { + if(len == 4 && memcmp(str, "auto", 4) == 0) { + *pixfmt = GSR_CAPTURE_V4L2_PIXFMT_AUTO; + return true; + } else if(len == 4 && memcmp(str, "yuyv", 4) == 0) { + *pixfmt = GSR_CAPTURE_V4L2_PIXFMT_YUYV; + return true; + } else if(len == 5 && memcmp(str, "mjpeg", 5) == 0) { + *pixfmt = GSR_CAPTURE_V4L2_PIXFMT_MJPEG; + return true; + } else { + return false; + } +} + +static bool string_to_bool(const char *str, size_t len, bool *value) { + if(len == 4 && memcmp(str, "true", 4) == 0) { + *value = true; + return true; + } else if(len == 5 && memcmp(str, "false", 5) == 0) { + *value = false; + return true; + } else { + return false; + } +} + +static bool parse_capture_source_options_callback(const char *sub, size_t size, void *userdata) { + parse_capture_source_options_userdata *parse_userdata = userdata; + gsr_capture_source *capture_source = parse_userdata->capture_source; + if(size == 0) + return true; + + /* First column contains the capture target */ + if(parse_userdata->is_first_column) { + parse_userdata->is_first_column = false; + return true; + } + + if(gsr_string_starts_with(sub, size, "x=")) { + capture_source->pos.x_type = sub[size - 1] == '%' ? VVEC2I_TYPE_SCALAR : VVEC2I_TYPE_PIXELS; + sub += 2; + size -= 2; + if(!gsr_string_to_int(sub, size, &capture_source->pos.x)) { + gsr_log(GSR_LOG_LEVEL_ERROR, "invalid capture target value for option x: \"%.*s\", expected a number", (int)size, sub); + parse_userdata->error = GSR_ERROR_GENERIC; + return false; + } + } else if(gsr_string_starts_with(sub, size, "y=")) { + capture_source->pos.y_type = sub[size - 1] == '%' ? VVEC2I_TYPE_SCALAR : VVEC2I_TYPE_PIXELS; + sub += 2; + size -= 2; + if(!gsr_string_to_int(sub, size, &capture_source->pos.y)) { + gsr_log(GSR_LOG_LEVEL_ERROR, "invalid capture target value for option y: \"%.*s\", expected a number", (int)size, sub); + parse_userdata->error = GSR_ERROR_GENERIC; + return false; + } + } else if(gsr_string_starts_with(sub, size, "width=")) { + capture_source->size.x_type = sub[size - 1] == '%' ? VVEC2I_TYPE_SCALAR : VVEC2I_TYPE_PIXELS; + sub += 6; + size -= 6; + if(!gsr_string_to_int(sub, size, &capture_source->size.x)) { + gsr_log(GSR_LOG_LEVEL_ERROR, "invalid capture target value for option width: \"%.*s\", expected a number", (int)size, sub); + parse_userdata->error = GSR_ERROR_GENERIC; + return false; + } + } else if(gsr_string_starts_with(sub, size, "height=")) { + capture_source->size.y_type = sub[size - 1] == '%' ? VVEC2I_TYPE_SCALAR : VVEC2I_TYPE_PIXELS; + sub += 7; + size -= 7; + if(!gsr_string_to_int(sub, size, &capture_source->size.y)) { + gsr_log(GSR_LOG_LEVEL_ERROR, "invalid capture target value for option height: \"%.*s\", expected a number", (int)size, sub); + parse_userdata->error = GSR_ERROR_GENERIC; + return false; + } + } else if(gsr_string_starts_with(sub, size, "halign=")) { + sub += 7; + size -= 7; + if(!string_to_capture_alignment(sub, size, &capture_source->halign)) { + gsr_log(GSR_LOG_LEVEL_ERROR, "invalid capture target value for option halign: \"%.*s\", expected a \"start\", \"center\" or \"end\"", (int)size, sub); + parse_userdata->error = GSR_ERROR_GENERIC; + return false; + } + } else if(gsr_string_starts_with(sub, size, "valign=")) { + sub += 7; + size -= 7; + if(!string_to_capture_alignment(sub, size, &capture_source->valign)) { + gsr_log(GSR_LOG_LEVEL_ERROR, "invalid capture target value for option valign: \"%.*s\", expected a \"start\", \"center\" or \"end\"", (int)size, sub); + parse_userdata->error = GSR_ERROR_GENERIC; + return false; + } + } else if(gsr_string_starts_with(sub, size, "pixfmt=")) { + sub += 7; + size -= 7; + if(!string_to_v4l2_pixfmt(sub, size, &capture_source->v4l2_pixfmt)) { + gsr_log(GSR_LOG_LEVEL_ERROR, "invalid v4l2 pixfmt value for option pixfmt: \"%.*s\", expected a \"auto\", \"yuyv\" or \"mjpeg\"", (int)size, sub); + parse_userdata->error = GSR_ERROR_GENERIC; + return false; + } + } else if(gsr_string_starts_with(sub, size, "hflip=")) { + sub += 6; + size -= 6; + bool hflip = false; + if(!string_to_bool(sub, size, &hflip)) { + gsr_log(GSR_LOG_LEVEL_ERROR, "invalid bool value for option hflip: \"%.*s\", expected a \"true\" or \"false\"", (int)size, sub); + parse_userdata->error = GSR_ERROR_GENERIC; + return false; + } + + if(hflip) + capture_source->flip |= GSR_FLIP_HORIZONTAL; + } else if(gsr_string_starts_with(sub, size, "vflip=")) { + sub += 6; + size -= 6; + bool vflip = false; + if(!string_to_bool(sub, size, &vflip)) { + gsr_log(GSR_LOG_LEVEL_ERROR, "invalid bool value for option vflip: \"%.*s\", expected a \"true\" or \"false\"", (int)size, sub); + parse_userdata->error = GSR_ERROR_GENERIC; + return false; + } + + if(vflip) + capture_source->flip |= GSR_FLIP_VERTICAL; + } else if(gsr_string_starts_with(sub, size, "camera_fps=")) { + sub += 11; + size -= 11; + if(!gsr_string_to_int(sub, size, &capture_source->camera_fps)) { + gsr_log(GSR_LOG_LEVEL_ERROR, "invalid capture target value for option camera_fps: \"%.*s\", expected a number", (int)size, sub); + parse_userdata->error = GSR_ERROR_GENERIC; + return false; + } + } else if(gsr_string_starts_with(sub, size, "camera_width=")) { + sub += 13; + size -= 13; + if(!gsr_string_to_int(sub, size, &capture_source->camera_resolution.x)) { + gsr_log(GSR_LOG_LEVEL_ERROR, "invalid capture target value for option camera_width: \"%.*s\", expected a number", (int)size, sub); + parse_userdata->error = GSR_ERROR_GENERIC; + return false; + } + } else if(gsr_string_starts_with(sub, size, "camera_height=")) { + sub += 14; + size -= 14; + if(!gsr_string_to_int(sub, size, &capture_source->camera_resolution.y)) { + gsr_log(GSR_LOG_LEVEL_ERROR, "invalid capture target value for option camera_height: \"%.*s\", expected a number", (int)size, sub); + parse_userdata->error = GSR_ERROR_GENERIC; + return false; + } + } else { + gsr_log(GSR_LOG_LEVEL_ERROR, "invalid capture target option \"%.*s\", expected x, y, width, height, halign, valign, pixfmt, hflip, vflip, camera_fps, camera_width or camera_height", (int)size, sub); + parse_userdata->error = GSR_ERROR_GENERIC; + return false; + } + + return true; +} + +static int parse_capture_source_options(const char *capture_source_str, size_t capture_source_str_size, gsr_capture_source *capture_source) { + char options[1024]; + snprintf(options, sizeof(options), "%.*s", (int)capture_source_str_size, capture_source_str); + + parse_capture_source_options_userdata userdata; + userdata.capture_source = capture_source; + userdata.is_first_column = true; + userdata.error = GSR_ERROR_OK; + gsr_string_split(options, ';', parse_capture_source_options_callback, &userdata); + return userdata.error; +} + +static bool parse_capture_source_arg_callback(const char *sub, size_t size, void *userdata) { + parse_capture_source_arg_userdata *parse_userdata = userdata; + if(size == 0) + return true; + + const char *substr_start = sub; + const size_t substr_size = size; + size_t capture_source_size = size; + const char *capture_source_end = memchr(sub, ';', size); + if(capture_source_end) + capture_source_size = capture_source_end - sub; + + gsr_capture_source capture_source; + gsr_capture_source_init(&capture_source, parse_userdata->region_position, parse_userdata->region_size); + + if(gsr_string_starts_with(sub, capture_source_size, "monitor:")) { + capture_source.type = GSR_CAPTURE_SOURCE_TYPE_MONITOR; + sub += 8; + capture_source_size -= 8; + } else if(gsr_string_starts_with(sub, capture_source_size, "window:")) { + capture_source.type = GSR_CAPTURE_SOURCE_TYPE_WINDOW; + sub += 7; + capture_source_size -= 7; + } else if(gsr_string_starts_with(sub, capture_source_size, "v4l2:")) { + capture_source.type = GSR_CAPTURE_SOURCE_TYPE_V4L2; + sub += 5; + capture_source_size -= 5; + } else { + capture_source_type_from_string(sub, capture_source_size, &capture_source); + } + + snprintf(capture_source.name, sizeof(capture_source.name), "%.*s", (int)capture_source_size, sub); + + if(capture_source.type == GSR_CAPTURE_SOURCE_TYPE_WINDOW) { + if(!gsr_string_to_int64(capture_source.name, strlen(capture_source.name), &capture_source.window_id)) { + gsr_log(GSR_LOG_LEVEL_ERROR, "invalid window number %s", capture_source.name); + args_parser_print_usage(); + parse_userdata->error = GSR_ERROR_GENERIC; + return false; + } + } + + if(parse_userdata->has_multiple_capture_sources) { + capture_source.halign = GSR_CAPTURE_ALIGN_START; + capture_source.valign = GSR_CAPTURE_ALIGN_START; + capture_source.pos = (vvec2i){0, 0, VVEC2I_TYPE_PIXELS, VVEC2I_TYPE_PIXELS}; + } + + const int parse_options_result = parse_capture_source_options(substr_start, substr_size, &capture_source); + if(parse_options_result != GSR_ERROR_OK) { + parse_userdata->error = parse_options_result; + return false; + } + + gsr_capture_sources *capture_sources = parse_userdata->capture_sources; + if(!gsr_array_ensure_capacity((void**)&capture_sources->items, capture_sources->num_items, &capture_sources->capacity_items, sizeof(gsr_capture_source))) { + parse_userdata->error = GSR_ERROR_GENERIC; + return false; + } + + capture_sources->items[capture_sources->num_items] = capture_source; + ++capture_sources->num_items; + return true; +} + +int gsr_capture_sources_parse(gsr_capture_sources *self, const char *capture_source_arg, vec2i region_position, vec2i region_size) { + memset(self, 0, sizeof(*self)); + + parse_capture_source_arg_userdata userdata; + userdata.capture_sources = self; + userdata.region_position = region_position; + userdata.region_size = region_size; + userdata.has_multiple_capture_sources = strchr(capture_source_arg, '|') != NULL; + userdata.error = GSR_ERROR_OK; + + gsr_string_split(capture_source_arg, '|', parse_capture_source_arg_callback, &userdata); + if(userdata.error != GSR_ERROR_OK) + gsr_capture_sources_deinit(self); + + return userdata.error; +} + +void gsr_capture_sources_deinit(gsr_capture_sources *self) { + if(self->items) { + free(self->items); + self->items = NULL; + } + self->num_items = 0; + self->capacity_items = 0; +} + +bool gsr_capture_sources_has_type(const gsr_capture_sources *self, CaptureSourceType type) { + for(size_t i = 0; i < self->num_items; ++i) { + if(self->items[i].type == type) + return true; + } + return false; +} + +bool gsr_capture_sources_has_damage_tracked_target(const gsr_capture_sources *self) { + for(size_t i = 0; i < self->num_items; ++i) { + if(self->items[i].type != GSR_CAPTURE_SOURCE_TYPE_V4L2) + return true; + } + return false; +} + +bool gsr_capture_sources_has_region_set(const gsr_capture_sources *self) { + for(size_t i = 0; i < self->num_items; ++i) { + if(self->items[i].type == GSR_CAPTURE_SOURCE_TYPE_REGION && self->items[i].region_set) + return true; + } + return false; +} + +bool gsr_capture_sources_has_monitor_or_region(const gsr_capture_sources *self) { + for(size_t i = 0; i < self->num_items; ++i) { + if(self->items[i].type == GSR_CAPTURE_SOURCE_TYPE_MONITOR || self->items[i].type == GSR_CAPTURE_SOURCE_TYPE_REGION) + return true; + } + return false; +} diff --git a/src/recorder/codec_select.c b/src/recorder/codec_select.c new file mode 100644 index 0000000..5f2bb68 --- /dev/null +++ b/src/recorder/codec_select.c @@ -0,0 +1,526 @@ +#include "../../include/recorder/codec_select.h" +#include "../../include/recorder/error.h" +#include "../../include/encoder/video/nvenc.h" +#include "../../include/encoder/video/vaapi.h" +#include "../../include/encoder/video/vulkan.h" +#include "../../include/encoder/video/software.h" +#include "../../include/codec_query/nvenc.h" +#include "../../include/codec_query/vaapi.h" +#include "../../include/codec_query/vulkan.h" +#include "../../include/log.h" + +#include <libavformat/avformat.h> + +#include <string.h> + +gsr_video_encoder* create_video_encoder(gsr_egl *egl, const gsr_recorder_settings *settings) { + const gsr_color_depth color_depth = video_codec_to_bit_depth(settings->video_codec); + gsr_video_encoder *video_encoder = NULL; + + if(settings->video_encoder == GSR_VIDEO_ENCODER_HW_CPU) { + gsr_video_encoder_software_params params; + params.egl = egl; + params.color_depth = color_depth; + video_encoder = gsr_video_encoder_software_create(¶ms); + return video_encoder; + } + + if(video_codec_is_vulkan(settings->video_codec)) { + gsr_video_encoder_vulkan_params params; + params.egl = egl; + params.color_depth = color_depth; + video_encoder = gsr_video_encoder_vulkan_create(¶ms); + return video_encoder; + } + + switch(egl->gpu_info.vendor) { + case GSR_GPU_VENDOR_AMD: + case GSR_GPU_VENDOR_INTEL: + case GSR_GPU_VENDOR_BROADCOM: + case GSR_GPU_VENDOR_APPLE: { + gsr_video_encoder_vaapi_params params; + params.egl = egl; + params.color_depth = color_depth; + video_encoder = gsr_video_encoder_vaapi_create(¶ms); + break; + } + case GSR_GPU_VENDOR_NVIDIA: { + gsr_video_encoder_nvenc_params params; + params.egl = egl; + params.color_depth = color_depth; + video_encoder = gsr_video_encoder_nvenc_create(¶ms); + break; + } + } + + return video_encoder; +} + +bool get_supported_video_codecs(gsr_egl *egl, gsr_video_codec video_codec, bool use_software_video_encoder, bool cleanup, gsr_supported_video_codecs *video_codecs) { + memset(video_codecs, 0, sizeof(*video_codecs)); + + if(use_software_video_encoder) { + video_codecs->h264.supported = avcodec_find_encoder_by_name("libx264"); + video_codecs->h264.max_resolution = (vec2i){4096, 2304}; + return true; + } + + if(video_codec_is_vulkan(video_codec)) + return gsr_get_supported_video_codecs_vulkan(video_codecs, egl->card_path, &egl->vulkan_device_index, cleanup); + + switch(egl->gpu_info.vendor) { + case GSR_GPU_VENDOR_AMD: + case GSR_GPU_VENDOR_INTEL: + case GSR_GPU_VENDOR_BROADCOM: + case GSR_GPU_VENDOR_APPLE: + return gsr_get_supported_video_codecs_vaapi(video_codecs, egl->card_path, cleanup); + case GSR_GPU_VENDOR_NVIDIA: + return gsr_get_supported_video_codecs_nvenc(video_codecs, cleanup); + } + + return false; +} + +static const AVCodec* get_ffmpeg_video_codec(gsr_video_codec video_codec, gsr_gpu_vendor vendor) { + switch(video_codec) { + case GSR_VIDEO_CODEC_H264: + return avcodec_find_encoder_by_name(vendor == GSR_GPU_VENDOR_NVIDIA ? "h264_nvenc" : "h264_vaapi"); + case GSR_VIDEO_CODEC_HEVC: + case GSR_VIDEO_CODEC_HEVC_HDR: + case GSR_VIDEO_CODEC_HEVC_10BIT: + return avcodec_find_encoder_by_name(vendor == GSR_GPU_VENDOR_NVIDIA ? "hevc_nvenc" : "hevc_vaapi"); + case GSR_VIDEO_CODEC_AV1: + case GSR_VIDEO_CODEC_AV1_HDR: + case GSR_VIDEO_CODEC_AV1_10BIT: + return avcodec_find_encoder_by_name(vendor == GSR_GPU_VENDOR_NVIDIA ? "av1_nvenc" : "av1_vaapi"); + case GSR_VIDEO_CODEC_VP8: + return avcodec_find_encoder_by_name(vendor == GSR_GPU_VENDOR_NVIDIA ? "vp8_nvenc" : "vp8_vaapi"); + case GSR_VIDEO_CODEC_VP9: + return avcodec_find_encoder_by_name(vendor == GSR_GPU_VENDOR_NVIDIA ? "vp9_nvenc" : "vp9_vaapi"); + case GSR_VIDEO_CODEC_H264_VULKAN: + return avcodec_find_encoder_by_name("h264_vulkan"); + case GSR_VIDEO_CODEC_HEVC_VULKAN: + case GSR_VIDEO_CODEC_HEVC_HDR_VULKAN: + case GSR_VIDEO_CODEC_HEVC_10BIT_VULKAN: + return avcodec_find_encoder_by_name("hevc_vulkan"); + case GSR_VIDEO_CODEC_AV1_VULKAN: + case GSR_VIDEO_CODEC_AV1_HDR_VULKAN: + case GSR_VIDEO_CODEC_AV1_10BIT_VULKAN: + return avcodec_find_encoder_by_name("av1_vulkan"); + } + return NULL; +} + +void set_supported_video_codecs_ffmpeg(gsr_supported_video_codecs *supported_video_codecs, gsr_supported_video_codecs *supported_video_codecs_vulkan, gsr_gpu_vendor vendor) { + if(!get_ffmpeg_video_codec(GSR_VIDEO_CODEC_H264, vendor)) { + supported_video_codecs->h264.supported = false; + } + + if(!get_ffmpeg_video_codec(GSR_VIDEO_CODEC_HEVC, vendor)) { + supported_video_codecs->hevc.supported = false; + supported_video_codecs->hevc_hdr.supported = false; + supported_video_codecs->hevc_10bit.supported = false; + } + + if(!get_ffmpeg_video_codec(GSR_VIDEO_CODEC_AV1, vendor)) { + supported_video_codecs->av1.supported = false; + supported_video_codecs->av1_hdr.supported = false; + supported_video_codecs->av1_10bit.supported = false; + } + + if(!get_ffmpeg_video_codec(GSR_VIDEO_CODEC_VP8, vendor)) { + supported_video_codecs->vp8.supported = false; + } + + if(!get_ffmpeg_video_codec(GSR_VIDEO_CODEC_VP9, vendor)) { + supported_video_codecs->vp9.supported = false; + } + + if(supported_video_codecs_vulkan) { + if(!get_ffmpeg_video_codec(GSR_VIDEO_CODEC_H264_VULKAN, vendor)) { + supported_video_codecs_vulkan->h264.supported = false; + } + + if(!get_ffmpeg_video_codec(GSR_VIDEO_CODEC_HEVC_VULKAN, vendor)) { + supported_video_codecs_vulkan->hevc.supported = false; + supported_video_codecs_vulkan->hevc_hdr.supported = false; + supported_video_codecs_vulkan->hevc_10bit.supported = false; + } + + if(!get_ffmpeg_video_codec(GSR_VIDEO_CODEC_AV1_VULKAN, vendor)) { + supported_video_codecs_vulkan->av1.supported = false; + supported_video_codecs_vulkan->av1_hdr.supported = false; + supported_video_codecs_vulkan->av1_10bit.supported = false; + } + } +} + +gsr_audio_codec select_audio_codec_with_fallback(gsr_audio_codec audio_codec, const char *file_extension, bool uses_amix) { + switch(audio_codec) { + case GSR_AUDIO_CODEC_AAC: { + if(strcmp(file_extension, "webm") == 0) { + //audio_codec_to_use = "opus"; + audio_codec = GSR_AUDIO_CODEC_OPUS; + gsr_log(GSR_LOG_LEVEL_WARNING, ".webm files only support opus audio codec, changing audio codec from aac to opus"); + } + break; + } + case GSR_AUDIO_CODEC_OPUS: { + if(strcmp(file_extension, "mp4") != 0 && strcmp(file_extension, "mkv") != 0 && strcmp(file_extension, "webm") != 0 && strcmp(file_extension, "ts") != 0 && strcmp(file_extension, "whip") != 0) { + //audio_codec_to_use = "aac"; + audio_codec = GSR_AUDIO_CODEC_AAC; + gsr_log(GSR_LOG_LEVEL_WARNING, "opus audio codec is only supported by .mp4, .mkv, .webm and .ts files, falling back to aac instead"); + } + break; + } + case GSR_AUDIO_CODEC_FLAC: { + // TODO: Also check mpegts? + if(strcmp(file_extension, "webm") == 0) { + //audio_codec_to_use = "opus"; + audio_codec = GSR_AUDIO_CODEC_OPUS; + gsr_log(GSR_LOG_LEVEL_WARNING, ".webm files only support opus audio codec, changing audio codec from flac to opus"); + } else if(strcmp(file_extension, "mp4") != 0 && strcmp(file_extension, "mkv") != 0) { + //audio_codec_to_use = "aac"; + audio_codec = GSR_AUDIO_CODEC_AAC; + gsr_log(GSR_LOG_LEVEL_WARNING, "flac audio codec is only supported by .mp4 and .mkv files, falling back to aac instead"); + } else if(uses_amix) { + // TODO: remove this? is it true anymore? + //audio_codec_to_use = "opus"; + audio_codec = GSR_AUDIO_CODEC_OPUS; + gsr_log(GSR_LOG_LEVEL_WARNING, "flac audio codec is not supported when mixing audio sources, falling back to opus instead"); + } + break; + } + } + return audio_codec; +} + +static bool video_codec_only_supports_low_power_mode(const gsr_supported_video_codecs *supported_video_codecs, gsr_video_codec video_codec) { + switch(video_codec) { + case GSR_VIDEO_CODEC_H264: return supported_video_codecs->h264.low_power; + case GSR_VIDEO_CODEC_HEVC: return supported_video_codecs->hevc.low_power; + case GSR_VIDEO_CODEC_HEVC_HDR: return supported_video_codecs->hevc_hdr.low_power; + case GSR_VIDEO_CODEC_HEVC_10BIT: return supported_video_codecs->hevc_10bit.low_power; + case GSR_VIDEO_CODEC_AV1: return supported_video_codecs->av1.low_power; + case GSR_VIDEO_CODEC_AV1_HDR: return supported_video_codecs->av1_hdr.low_power; + case GSR_VIDEO_CODEC_AV1_10BIT: return supported_video_codecs->av1_10bit.low_power; + case GSR_VIDEO_CODEC_VP8: return supported_video_codecs->vp8.low_power; + case GSR_VIDEO_CODEC_VP9: return supported_video_codecs->vp9.low_power; + case GSR_VIDEO_CODEC_H264_VULKAN: return supported_video_codecs->h264.low_power; + case GSR_VIDEO_CODEC_HEVC_VULKAN: return supported_video_codecs->hevc.low_power; + case GSR_VIDEO_CODEC_HEVC_HDR_VULKAN: return supported_video_codecs->hevc_hdr.low_power; + case GSR_VIDEO_CODEC_HEVC_10BIT_VULKAN: return supported_video_codecs->hevc_10bit.low_power; + case GSR_VIDEO_CODEC_AV1_VULKAN: return supported_video_codecs->av1.low_power; + case GSR_VIDEO_CODEC_AV1_HDR_VULKAN: return supported_video_codecs->av1_hdr.low_power; + case GSR_VIDEO_CODEC_AV1_10BIT_VULKAN: return supported_video_codecs->av1_10bit.low_power; + } + return false; +} + +static const AVCodec* get_av_codec_if_supported(gsr_video_codec video_codec, gsr_egl *egl, bool use_software_video_encoder, const gsr_supported_video_codecs *supported_video_codecs) { + switch(video_codec) { + case GSR_VIDEO_CODEC_H264: + case GSR_VIDEO_CODEC_H264_VULKAN: { + if(use_software_video_encoder) + return avcodec_find_encoder_by_name("libx264"); + else if(supported_video_codecs->h264.supported) + return get_ffmpeg_video_codec(video_codec, egl->gpu_info.vendor); + break; + } + case GSR_VIDEO_CODEC_HEVC: + case GSR_VIDEO_CODEC_HEVC_VULKAN: { + if(supported_video_codecs->hevc.supported) + return get_ffmpeg_video_codec(video_codec, egl->gpu_info.vendor); + break; + } + case GSR_VIDEO_CODEC_HEVC_HDR: + case GSR_VIDEO_CODEC_HEVC_HDR_VULKAN: { + if(supported_video_codecs->hevc_hdr.supported) + return get_ffmpeg_video_codec(video_codec, egl->gpu_info.vendor); + break; + } + case GSR_VIDEO_CODEC_HEVC_10BIT: + case GSR_VIDEO_CODEC_HEVC_10BIT_VULKAN: { + if(supported_video_codecs->hevc_10bit.supported) + return get_ffmpeg_video_codec(video_codec, egl->gpu_info.vendor); + break; + } + case GSR_VIDEO_CODEC_AV1: + case GSR_VIDEO_CODEC_AV1_VULKAN: { + if(supported_video_codecs->av1.supported) + return get_ffmpeg_video_codec(video_codec, egl->gpu_info.vendor); + break; + } + case GSR_VIDEO_CODEC_AV1_HDR: + case GSR_VIDEO_CODEC_AV1_HDR_VULKAN: { + if(supported_video_codecs->av1_hdr.supported) + return get_ffmpeg_video_codec(video_codec, egl->gpu_info.vendor); + break; + } + case GSR_VIDEO_CODEC_AV1_10BIT: + case GSR_VIDEO_CODEC_AV1_10BIT_VULKAN: { + if(supported_video_codecs->av1_10bit.supported) + return get_ffmpeg_video_codec(video_codec, egl->gpu_info.vendor); + break; + } + case GSR_VIDEO_CODEC_VP8: { + if(supported_video_codecs->vp8.supported) + return get_ffmpeg_video_codec(video_codec, egl->gpu_info.vendor); + break; + } + case GSR_VIDEO_CODEC_VP9: { + if(supported_video_codecs->vp9.supported) + return get_ffmpeg_video_codec(video_codec, egl->gpu_info.vendor); + break; + } + } + return NULL; +} + +vec2i codec_get_max_resolution(gsr_video_codec video_codec, bool use_software_video_encoder, const gsr_supported_video_codecs *supported_video_codecs) { + switch(video_codec) { + case GSR_VIDEO_CODEC_H264: + case GSR_VIDEO_CODEC_H264_VULKAN: { + if(use_software_video_encoder) + return (vec2i){4096, 2304}; + else if(supported_video_codecs->h264.supported) + return supported_video_codecs->h264.max_resolution; + break; + } + case GSR_VIDEO_CODEC_HEVC: + case GSR_VIDEO_CODEC_HEVC_VULKAN: { + if(supported_video_codecs->hevc.supported) + return supported_video_codecs->hevc.max_resolution; + break; + } + case GSR_VIDEO_CODEC_HEVC_HDR: + case GSR_VIDEO_CODEC_HEVC_HDR_VULKAN: { + if(supported_video_codecs->hevc_hdr.supported) + return supported_video_codecs->hevc_hdr.max_resolution; + break; + } + case GSR_VIDEO_CODEC_HEVC_10BIT: + case GSR_VIDEO_CODEC_HEVC_10BIT_VULKAN: { + if(supported_video_codecs->hevc_10bit.supported) + return supported_video_codecs->hevc_10bit.max_resolution; + break; + } + case GSR_VIDEO_CODEC_AV1: + case GSR_VIDEO_CODEC_AV1_VULKAN: { + if(supported_video_codecs->av1.supported) + return supported_video_codecs->av1.max_resolution; + break; + } + case GSR_VIDEO_CODEC_AV1_HDR: + case GSR_VIDEO_CODEC_AV1_HDR_VULKAN: { + if(supported_video_codecs->av1_hdr.supported) + return supported_video_codecs->av1_hdr.max_resolution; + break; + } + case GSR_VIDEO_CODEC_AV1_10BIT: + case GSR_VIDEO_CODEC_AV1_10BIT_VULKAN: { + if(supported_video_codecs->av1_10bit.supported) + return supported_video_codecs->av1_10bit.max_resolution; + break; + } + case GSR_VIDEO_CODEC_VP8: { + if(supported_video_codecs->vp8.supported) + return supported_video_codecs->vp8.max_resolution; + break; + } + case GSR_VIDEO_CODEC_VP9: { + if(supported_video_codecs->vp9.supported) + return supported_video_codecs->vp9.max_resolution; + break; + } + } + return (vec2i){0, 0}; +} + +bool codec_supports_resolution(vec2i codec_max_resolution, vec2i capture_resolution) { + if(codec_max_resolution.x == 0 || codec_max_resolution.y == 0) + return true; + return codec_max_resolution.x >= capture_resolution.x && codec_max_resolution.y >= capture_resolution.y; +} + +static void print_codec_error(gsr_video_codec video_codec) { + if(video_codec == (gsr_video_codec)GSR_VIDEO_CODEC_AUTO) + video_codec = GSR_VIDEO_CODEC_H264; + + const char *video_codec_name = video_codec_to_string(video_codec); + gsr_log(GSR_LOG_LEVEL_ERROR, "your gpu does not support '%s' video codec. If you are sure that your gpu does support '%s' video encoding and you are using an AMD/Intel GPU,\n then make sure you have installed the GPU specific vaapi packages (intel-media-driver, libva-intel-driver, libva-mesa-driver and linux-firmware).\n It's also possible that your distro has disabled hardware accelerated video encoding for '%s' video codec.\n This may be the case on corporate distros such as Manjaro, Fedora or OpenSUSE.\n You can test this by running 'vainfo | grep VAEntrypointEncSlice' to see if it matches any H264/HEVC/AV1/VP8/VP9 profile.\n On such distros, you need to manually install mesa from source to enable H264/HEVC hardware acceleration, or use a more user friendly distro. Alternatively record with AV1 if supported by your GPU.\n You can alternatively use the flatpak version of GPU Screen Recorder (https://flathub.org/apps/com.dec05eba.gpu_screen_recorder) which bypasses system issues with patented H264/HEVC codecs.\n If your GPU doesn't support hardware accelerated video encoding then you can use '-fallback-cpu-encoding yes' option to encode with your cpu instead.", video_codec_name, video_codec_name, video_codec_name); +} + +void force_cpu_encoding(gsr_recorder_settings *settings) { + settings->video_codec = GSR_VIDEO_CODEC_H264; + settings->video_encoder = GSR_VIDEO_ENCODER_HW_CPU; + if(settings->bitrate_mode == GSR_BITRATE_MODE_VBR) { + gsr_log(GSR_LOG_LEVEL_WARNING, "bitrate mode has been forcefully set to qp because software encoding option doesn't support vbr option"); + settings->bitrate_mode = GSR_BITRATE_MODE_QP; + } +} + +static int pick_video_codec(gsr_egl *egl, gsr_recorder_settings *settings, bool use_fallback_codec, bool *low_power, gsr_supported_video_codecs *supported_video_codecs, const AVCodec **video_codec) { + // TODO: software encoder for hevc, av1, vp8 and vp9 + *video_codec = NULL; + *low_power = false; + const AVCodec *video_codec_f = get_av_codec_if_supported(settings->video_codec, egl, settings->video_encoder == GSR_VIDEO_ENCODER_HW_CPU, supported_video_codecs); + + if(!video_codec_f && use_fallback_codec && settings->video_encoder != GSR_VIDEO_ENCODER_HW_CPU) { + switch(settings->video_codec) { + case GSR_VIDEO_CODEC_H264: { + gsr_log(GSR_LOG_LEVEL_ERROR, "selected video codec h264 is not supported by your hardware"); + if(settings->fallback_cpu_encoding) { + gsr_log(GSR_LOG_LEVEL_WARNING, "gpu encoding is not available on your system, trying cpu encoding instead because -fallback-cpu-encoding is enabled. Install the proper vaapi drivers on your system (if supported) if you experience performance issues"); + force_cpu_encoding(settings); + } + break; + } + case GSR_VIDEO_CODEC_HEVC: + case GSR_VIDEO_CODEC_HEVC_HDR: + case GSR_VIDEO_CODEC_HEVC_10BIT: { + gsr_log(GSR_LOG_LEVEL_WARNING, "selected video codec hevc is not supported by your hardware, trying h264 instead"); + settings->video_codec = GSR_VIDEO_CODEC_H264; + return pick_video_codec(egl, settings, true, low_power, supported_video_codecs, video_codec); + } + case GSR_VIDEO_CODEC_AV1: + case GSR_VIDEO_CODEC_AV1_HDR: + case GSR_VIDEO_CODEC_AV1_10BIT: { + gsr_log(GSR_LOG_LEVEL_WARNING, "selected video codec av1 is not supported by your hardware, trying h264 instead"); + settings->video_codec = GSR_VIDEO_CODEC_H264; + return pick_video_codec(egl, settings, true, low_power, supported_video_codecs, video_codec); + } + case GSR_VIDEO_CODEC_VP8: + case GSR_VIDEO_CODEC_VP9: + // TODO: Cant fallback to other codec because webm only supports vp8/vp9 + break; + case GSR_VIDEO_CODEC_H264_VULKAN: { + gsr_log(GSR_LOG_LEVEL_WARNING, "selected video codec h264_vulkan is not supported by your hardware, trying h264 instead"); + settings->video_codec = GSR_VIDEO_CODEC_H264; + // Need to do a query again because this time it's without vulkan + if(!get_supported_video_codecs(egl, settings->video_codec, false, true, supported_video_codecs)) { + gsr_log(GSR_LOG_LEVEL_ERROR, "failed to query for supported video codecs"); + print_codec_error(settings->video_codec); + return GSR_ERROR_VIDEO_CODEC_QUERY_FAILED; + } + return pick_video_codec(egl, settings, true, low_power, supported_video_codecs, video_codec); + } + case GSR_VIDEO_CODEC_HEVC_VULKAN: + case GSR_VIDEO_CODEC_HEVC_HDR_VULKAN: + case GSR_VIDEO_CODEC_HEVC_10BIT_VULKAN: { + gsr_log(GSR_LOG_LEVEL_WARNING, "selected video codec hevc_vulkan is not supported by your hardware, trying hevc instead"); + settings->video_codec = GSR_VIDEO_CODEC_HEVC; + // Need to do a query again because this time it's without vulkan + if(!get_supported_video_codecs(egl, settings->video_codec, false, true, supported_video_codecs)) { + gsr_log(GSR_LOG_LEVEL_ERROR, "failed to query for supported video codecs"); + print_codec_error(settings->video_codec); + return GSR_ERROR_VIDEO_CODEC_QUERY_FAILED; + } + return pick_video_codec(egl, settings, true, low_power, supported_video_codecs, video_codec); + } + case GSR_VIDEO_CODEC_AV1_VULKAN: + case GSR_VIDEO_CODEC_AV1_HDR_VULKAN: + case GSR_VIDEO_CODEC_AV1_10BIT_VULKAN: { + gsr_log(GSR_LOG_LEVEL_WARNING, "selected video codec av1_vulkan is not supported by your hardware, trying av1 instead"); + settings->video_codec = GSR_VIDEO_CODEC_AV1; + // Need to do a query again because this time it's without vulkan + if(!get_supported_video_codecs(egl, settings->video_codec, false, true, supported_video_codecs)) { + gsr_log(GSR_LOG_LEVEL_ERROR, "failed to query for supported video codecs"); + print_codec_error(settings->video_codec); + return GSR_ERROR_VIDEO_CODEC_QUERY_FAILED; + } + return pick_video_codec(egl, settings, true, low_power, supported_video_codecs, video_codec); + } + } + + video_codec_f = get_av_codec_if_supported(settings->video_codec, egl, settings->video_encoder == GSR_VIDEO_ENCODER_HW_CPU, supported_video_codecs); + } + + if(!video_codec_f) { + print_codec_error(settings->video_codec); + return GSR_ERROR_VIDEO_CODEC_UNSUPPORTED; + } + + *low_power = video_codec_only_supports_low_power_mode(supported_video_codecs, settings->video_codec); + *video_codec = video_codec_f; + return GSR_ERROR_OK; +} + +gsr_video_codec select_appropriate_video_codec_automatically(vec2i video_size, const gsr_supported_video_codecs *supported_video_codecs) { + if(supported_video_codecs->h264.supported && codec_supports_resolution(supported_video_codecs->h264.max_resolution, video_size)) { + gsr_log(GSR_LOG_LEVEL_INFO, "using h264 encoder because a codec was not specified"); + return GSR_VIDEO_CODEC_H264; + } else if(supported_video_codecs->hevc.supported && codec_supports_resolution(supported_video_codecs->hevc.max_resolution, video_size)) { + gsr_log(GSR_LOG_LEVEL_INFO, "using hevc encoder because a codec was not specified and h264 supported max resolution (%dx%d) is less than the capture resolution (%dx%d)", supported_video_codecs->h264.max_resolution.x, supported_video_codecs->h264.max_resolution.y, + video_size.x, video_size.y); + return GSR_VIDEO_CODEC_HEVC; + } else if(supported_video_codecs->av1.supported && codec_supports_resolution(supported_video_codecs->av1.max_resolution, video_size)) { + gsr_log(GSR_LOG_LEVEL_INFO, "using av1 encoder because a codec was not specified and hevc supported max resolution (%dx%d) is less than the capture resolution (%dx%d)", supported_video_codecs->hevc.max_resolution.x, supported_video_codecs->hevc.max_resolution.y, + video_size.x, video_size.y); + return GSR_VIDEO_CODEC_AV1; + } else { + return (gsr_video_codec)-1; + } +} + +int select_video_codec_with_fallback(vec2i video_size, gsr_recorder_settings *settings, const char *file_extension, gsr_egl *egl, bool *low_power, const AVCodec **video_codec) { + gsr_supported_video_codecs supported_video_codecs_non_vulkan; + get_supported_video_codecs(egl, settings->video_codec, settings->video_encoder == GSR_VIDEO_ENCODER_HW_CPU, true, &supported_video_codecs_non_vulkan); + + gsr_supported_video_codecs supported_video_codecs_vulkan = supported_video_codecs_non_vulkan; + set_supported_video_codecs_ffmpeg(&supported_video_codecs_non_vulkan, &supported_video_codecs_vulkan, egl->gpu_info.vendor); + + gsr_supported_video_codecs *supported_video_codecs = video_codec_is_vulkan(settings->video_codec) + ? &supported_video_codecs_vulkan + : &supported_video_codecs_non_vulkan; + + const bool video_codec_auto = settings->video_codec == (gsr_video_codec)GSR_VIDEO_CODEC_AUTO; + if(video_codec_auto) { + if(strcmp(file_extension, "webm") == 0) { + gsr_log(GSR_LOG_LEVEL_INFO, "using vp8 encoder because a codec was not specified and the file extension is .webm"); + settings->video_codec = GSR_VIDEO_CODEC_VP8; + } else if(settings->video_encoder == GSR_VIDEO_ENCODER_HW_CPU) { + gsr_log(GSR_LOG_LEVEL_INFO, "using h264 encoder because a codec was not specified"); + settings->video_codec = GSR_VIDEO_CODEC_H264; + } else if(settings->video_encoder != GSR_VIDEO_ENCODER_HW_CPU) { + settings->video_codec = select_appropriate_video_codec_automatically(video_size, &supported_video_codecs_non_vulkan); + if(settings->video_codec == (gsr_video_codec)-1) { + if(settings->fallback_cpu_encoding) { + gsr_log(GSR_LOG_LEVEL_WARNING, "gpu encoding is not available on your system or your gpu doesn't support recording at the resolution you are trying to record, trying cpu encoding instead because -fallback-cpu-encoding is enabled. Install the proper vaapi drivers on your system (if supported) if you experience performance issues"); + force_cpu_encoding(settings); + } else { + gsr_log(GSR_LOG_LEVEL_ERROR, "no video encoder was specified and neither h264, hevc nor av1 are supported on your system or you are trying to capture at a resolution higher than your system supports for each codec.\n" + " Ensure that you have installed the proper vaapi driver. If your gpu doesn't support video encoding then you can run gpu-screen-recorder with \"-fallback-cpu-encoding yes\" option to use cpu encoding."); + return GSR_ERROR_NO_VIDEO_CODEC_AVAILABLE; + } + } + } + } + + if(LIBAVFORMAT_VERSION_INT < AV_VERSION_INT(60, 10, 100) && strcmp(file_extension, "flv") == 0) { + if(settings->video_codec != GSR_VIDEO_CODEC_H264) { + settings->video_codec = GSR_VIDEO_CODEC_H264; + gsr_log(GSR_LOG_LEVEL_WARNING, "hevc/av1 is not compatible with flv in your outdated version of ffmpeg, falling back to h264 instead."); + } + } else if(strcmp(file_extension, "m3u8") == 0) { + if(video_codec_is_av1(settings->video_codec)) { + settings->video_codec = GSR_VIDEO_CODEC_HEVC; + gsr_log(GSR_LOG_LEVEL_WARNING, "av1 is not compatible with hls (m3u8), falling back to hevc instead."); + } + } + + const AVCodec *codec = NULL; + const int pick_codec_result = pick_video_codec(egl, settings, true, low_power, supported_video_codecs, &codec); + if(pick_codec_result != GSR_ERROR_OK) + return pick_codec_result; + + const vec2i codec_max_resolution = codec_get_max_resolution(settings->video_codec, settings->video_encoder == GSR_VIDEO_ENCODER_HW_CPU, supported_video_codecs); + if(!codec_supports_resolution(codec_max_resolution, video_size)) { + const char *video_codec_name = video_codec_to_string(settings->video_codec); + gsr_log(GSR_LOG_LEVEL_ERROR, "The max resolution for video codec %s is %dx%d while you are trying to capture at resolution %dx%d. Change capture resolution or video codec and try again", video_codec_name, codec_max_resolution.x, codec_max_resolution.y, video_size.x, video_size.y); + return GSR_ERROR_VIDEO_CODEC_RESOLUTION_UNSUPPORTED; + } + + *video_codec = codec; + return GSR_ERROR_OK; +} diff --git a/src/recorder/muxer.c b/src/recorder/muxer.c new file mode 100644 index 0000000..806921d --- /dev/null +++ b/src/recorder/muxer.c @@ -0,0 +1,268 @@ +#include "../../include/recorder/muxer.h" +#include "../../include/recorder/audio_codec.h" +#include "../../include/ffmpeg_utils.h" +#include "../../include/utils.h" +#include "../../include/log.h" + +#include <string.h> +#include <stdlib.h> +#include <stdio.h> + +#include <libavutil/opt.h> +#include <libavutil/mastering_display_metadata.h> + +static void gsr_recording_output_deinit(gsr_recording_output *self) { + if(self->audio_streams) { + free(self->audio_streams); + self->audio_streams = NULL; + } + self->num_audio_streams = 0; +} + +AVStream* create_stream(AVFormatContext *av_format_context, AVCodecContext *codec_context) { + AVStream *stream = avformat_new_stream(av_format_context, NULL); + if (!stream) { + gsr_log(GSR_LOG_LEVEL_ERROR, "Could not allocate stream"); + return NULL; + } + stream->id = av_format_context->nb_streams - 1; + stream->time_base = codec_context->time_base; + stream->avg_frame_rate = codec_context->framerate; + //stream->r_frame_rate = codec_context->framerate; + return stream; +} + +bool add_hdr_metadata_to_video_stream(gsr_capture *cap, AVStream *video_stream) { + size_t light_metadata_size = 0; + size_t mastering_display_metadata_size = 0; + AVContentLightMetadata *light_metadata = av_content_light_metadata_alloc(&light_metadata_size); + #if LIBAVUTIL_VERSION_INT < AV_VERSION_INT(59, 37, 100) + AVMasteringDisplayMetadata *mastering_display_metadata = av_mastering_display_metadata_alloc(); + mastering_display_metadata_size = sizeof(*mastering_display_metadata); + #else + AVMasteringDisplayMetadata *mastering_display_metadata = av_mastering_display_metadata_alloc_size(&mastering_display_metadata_size); + #endif + + if(!light_metadata || !mastering_display_metadata) { + if(light_metadata) + av_freep(&light_metadata); + + if(mastering_display_metadata) + av_freep(&mastering_display_metadata); + + return false; + } + + if(!gsr_capture_set_hdr_metadata(cap, mastering_display_metadata, light_metadata)) { + av_freep(&light_metadata); + av_freep(&mastering_display_metadata); + return false; + } + + // TODO: More error checking + + #if LIBAVCODEC_VERSION_INT < AV_VERSION_INT(60, 31, 102) + const bool content_light_level_added = av_stream_add_side_data(video_stream, AV_PKT_DATA_CONTENT_LIGHT_LEVEL, (uint8_t*)light_metadata, light_metadata_size) == 0; + #else + const bool content_light_level_added = av_packet_side_data_add(&video_stream->codecpar->coded_side_data, &video_stream->codecpar->nb_coded_side_data, AV_PKT_DATA_CONTENT_LIGHT_LEVEL, light_metadata, light_metadata_size, 0) != NULL; + #endif + + #if LIBAVCODEC_VERSION_INT < AV_VERSION_INT(60, 31, 102) + const bool mastering_display_metadata_added = av_stream_add_side_data(video_stream, AV_PKT_DATA_MASTERING_DISPLAY_METADATA, (uint8_t*)mastering_display_metadata, mastering_display_metadata_size) == 0; + #else + const bool mastering_display_metadata_added = av_packet_side_data_add(&video_stream->codecpar->coded_side_data, &video_stream->codecpar->nb_coded_side_data, AV_PKT_DATA_MASTERING_DISPLAY_METADATA, mastering_display_metadata, mastering_display_metadata_size, 0) != NULL; + #endif + + if(!content_light_level_added) + av_freep(&light_metadata); + + if(!mastering_display_metadata_added) + av_freep(&mastering_display_metadata); + + // Return true even on failure because we dont want to retry adding hdr metadata on failure + return true; +} + +void set_format_context_options(AVFormatContext *av_format_context) { + if(LIBAVFORMAT_VERSION_INT >= AV_VERSION_INT(62, 6, 101)) { + av_opt_set(av_format_context->priv_data, "use_editlist", "1", 0); + const AVOption *opt = av_opt_find(av_format_context->priv_data, "movflags", NULL, 0, 0); + if (opt && opt->unit) { + const AVOption *flag = av_opt_find(av_format_context->priv_data, "hybrid_fragmented", opt->unit, 0, 0); + if (flag) + av_opt_set(av_format_context->priv_data, "movflags", "+hybrid_fragmented", 0); + } + } else { + const AVOutputFormat *output_format = av_format_context->oformat; + const char *file_extension = output_format->extensions ? output_format->extensions : ""; + if(strcmp(file_extension, "mp4") != 0 && strcmp(file_extension, "mov") != 0) + return; + + gsr_log(GSR_LOG_LEVEL_WARNING, "your FFmpeg version is known to be buggy (it doesn't have working hybrid_fragmented movflags). If you experience stutter in the mp4 file then update your FFmpeg to at least version 8 or record to a mkv file"); + } +} + +void av_write_header(AVFormatContext *av_format_context, const char *ffmpeg_opts) { + AVDictionary *options = NULL; + av_dict_set(&options, "strict", "experimental", 0); + + if(ffmpeg_opts) + av_dict_parse_string(&options, ffmpeg_opts, "=", ";", 0); + + const int ret = avformat_write_header(av_format_context, &options); + if(ret < 0) + gsr_log(GSR_LOG_LEVEL_ERROR, "error occurred when writing header to output file: %s", gsr_av_error_to_string(ret)); + + av_dict_free(&options); +} + +bool gsr_recording_output_start(gsr_recording_output *self, const char *filename, const gsr_recorder_settings *settings, AVCodecContext *video_codec_context, const gsr_audio_capture *audio_capture, bool hdr, gsr_video_sources *video_sources) { + memset(self, 0, sizeof(*self)); + + AVFormatContext *av_format_context = NULL; + avformat_alloc_output_context2(&av_format_context, NULL, settings->container_format, filename); + if(!av_format_context) { + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_recording_output_start: failed to create output context for '%s'", filename); + return false; + } + set_format_context_options(av_format_context); + + AVStream *video_stream = create_stream(av_format_context, video_codec_context); + if(!video_stream) { + avformat_free_context(av_format_context); + return false; + } + avcodec_parameters_from_context(video_stream->codecpar, video_codec_context); + + if(audio_capture->num_tracks > 0) { + self->audio_streams = calloc(audio_capture->num_tracks, sizeof(gsr_recording_audio_stream)); + if(!self->audio_streams) { + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_recording_output_start: failed to allocate audio streams"); + avformat_free_context(av_format_context); + return false; + } + } + + for(size_t i = 0; i < audio_capture->num_tracks; ++i) { + const gsr_audio_track *audio_track = &audio_capture->tracks[i]; + AVStream *audio_stream = create_stream(av_format_context, audio_track->codec_context); + if(!audio_stream) { + gsr_recording_output_deinit(self); + avformat_free_context(av_format_context); + return false; + } + + if(audio_track->name[0] != '\0' && !settings->exclude_metadata) + av_dict_set(&audio_stream->metadata, "title", audio_track->name, 0); + avcodec_parameters_from_context(audio_stream->codecpar, audio_track->codec_context); + + self->audio_streams[i].audio_track = audio_track; + self->audio_streams[i].stream = audio_stream; + ++self->num_audio_streams; + } + + const int open_ret = avio_open(&av_format_context->pb, filename, AVIO_FLAG_WRITE); + if(open_ret < 0) { + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_recording_output_start: could not open '%s': %s", filename, gsr_av_error_to_string(open_ret)); + gsr_recording_output_deinit(self); + avformat_free_context(av_format_context); + return false; + } + + AVDictionary *options = NULL; + av_dict_set(&options, "strict", "experimental", 0); + + if(settings->ffmpeg_opts) + av_dict_parse_string(&options, settings->ffmpeg_opts, "=", ";", 0); + + const int header_write_ret = avformat_write_header(av_format_context, &options); + av_dict_free(&options); + if(header_write_ret < 0) { + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_recording_output_start: error occurred when writing header to output file: %s", gsr_av_error_to_string(header_write_ret)); + avio_close(av_format_context->pb); + gsr_recording_output_deinit(self); + avformat_free_context(av_format_context); + return false; + } + + for(size_t i = 0; i < video_sources->num_items; ++i) { + if(hdr && add_hdr_metadata_to_video_stream(video_sources->items[i].capture, video_stream)) + break; + } + + self->av_format_context = av_format_context; + self->video_stream = video_stream; + return true; +} + +bool gsr_recording_output_stop(gsr_recording_output *self) { + bool trailer_written = true; + if(gsr_av_format_context_write_trailer(self->av_format_context) != 0) { + //trailer_written = false; + } + + const bool closed = avio_close(self->av_format_context->pb) == 0; + avformat_free_context(self->av_format_context); + self->av_format_context = NULL; + self->video_stream = NULL; + gsr_recording_output_deinit(self); + return trailer_written && closed; +} + +bool gsr_create_new_recording_filepath_from_timestamp(char *filepath, size_t filepath_size, const char *directory, const char *filename_prefix, const char *file_extension, bool date_folders) { + char date_str[128]; + char output_folder[PATH_MAX]; + int written = 0; + + if(date_folders) { + gsr_get_date_only_str(date_str, sizeof(date_str)); + written = snprintf(output_folder, sizeof(output_folder), "%s/%s", directory, date_str); + if(written < 0 || written >= (int)sizeof(output_folder)) { + gsr_log(GSR_LOG_LEVEL_ERROR, "the directory path is too long: %s", directory); + return false; + } + + if(create_directory_recursive(output_folder) != 0) + gsr_log(GSR_LOG_LEVEL_ERROR, "failed to create directory: %s", output_folder); + + gsr_get_time_only_str(date_str, sizeof(date_str)); + } else { + written = snprintf(output_folder, sizeof(output_folder), "%s", directory); + if(written < 0 || written >= (int)sizeof(output_folder)) { + gsr_log(GSR_LOG_LEVEL_ERROR, "the directory path is too long: %s", directory); + return false; + } + + if(create_directory_recursive(output_folder) != 0) + gsr_log(GSR_LOG_LEVEL_ERROR, "failed to create directory: %s", output_folder); + + gsr_get_date_str(date_str, sizeof(date_str)); + } + + written = snprintf(filepath, filepath_size, "%s/%s_%s.%s", output_folder, filename_prefix, date_str, file_extension); + if(written < 0 || written >= (int)filepath_size) { + gsr_log(GSR_LOG_LEVEL_ERROR, "the output filepath is too long"); + return false; + } + + return true; +} + +gsr_recording_audio_stream* gsr_recording_output_get_audio_stream_by_index(gsr_recording_output *self, int stream_index) { + for(size_t i = 0; i < self->num_audio_streams; ++i) { + if(self->audio_streams[i].stream->index == stream_index) + return &self->audio_streams[i]; + } + return NULL; +} + +size_t calculate_estimated_replay_buffer_packets(int64_t replay_buffer_size_secs, int fps, gsr_audio_codec audio_codec, const gsr_audio_input_tracks *audio_inputs) { + if(replay_buffer_size_secs == -1) + return 0; + + int audio_fps = 0; + if(audio_inputs->num_items > 0) + audio_fps = GSR_AUDIO_SAMPLE_RATE / audio_codec_get_frame_size(audio_codec); + + return replay_buffer_size_secs * (fps + audio_fps * audio_inputs->num_items); +} diff --git a/src/recorder/recorder.c b/src/recorder/recorder.c new file mode 100644 index 0000000..29d36d8 --- /dev/null +++ b/src/recorder/recorder.c @@ -0,0 +1,969 @@ +#include "../../include/recorder/recorder.h" +#include "../../include/recorder/error.h" +#include "../../include/recorder/audio_codec.h" +#include "../../include/recorder/video_codec.h" +#include "../../include/recorder/codec_select.h" +#include "../../include/recorder/muxer.h" +#include "../../include/recorder/replay_save.h" +#include "../../include/recorder/audio_capture.h" +#include "../../include/recorder/recording_clock.h" +#include "../../include/recorder/screenshot.h" +#include "../../include/encoder/encoder.h" +#include "../../include/encoder/video/video.h" +#include "../../include/window/window.h" +#include "../../include/color_conversion.h" +#include "../../include/damage.h" +#include "../../include/cursor.h" +#include "../../include/plugins.h" +#include "../../include/utils.h" +#include "../../include/ffmpeg_utils.h" +#include "../../include/log.h" + +#include <string.h> +#include <stdlib.h> +#include <stdio.h> +#include <math.h> +#include <assert.h> +#include <limits.h> +#include <unistd.h> +#include <stdatomic.h> + +#include <libavutil/time.h> +#include <libavformat/avformat.h> + +#include <X11/Xlib.h> + +#define GSR_VIDEO_STREAM_INDEX 0 + +#define GSR_SET_PAUSED_REQUEST_NONE -1 +#define GSR_SET_PAUSED_REQUEST_UNPAUSE 0 +#define GSR_SET_PAUSED_REQUEST_PAUSE 1 + +#define GSR_REPLAY_RECORDING_REQUEST_NONE 0 +#define GSR_REPLAY_RECORDING_REQUEST_TOGGLE 1 +#define GSR_REPLAY_RECORDING_REQUEST_START 2 +#define GSR_REPLAY_RECORDING_REQUEST_STOP 3 + +struct gsr_recorder { + gsr_recorder_settings settings; + gsr_recorder_callbacks callbacks; + gsr_windowing *windowing; + gsr_egl *egl; + gsr_window *window; + gsr_capture_deps *capture_deps; + gsr_capture_sources *capture_sources; + gsr_audio_input_tracks *audio_input_tracks; + + char file_extension[32]; + bool force_no_audio_offset; + double target_fps; + bool uses_amix; + bool hdr; + bool low_power; + vec2i video_size; + + AVFormatContext *av_format_context; + AVStream *video_stream; + AVCodecContext *video_codec_context; + AVFrame *video_frame; + gsr_video_sources video_sources_data; + gsr_video_sources *video_sources; + gsr_encoder encoder; + bool encoder_initialized; + gsr_video_encoder *video_encoder; + gsr_color_conversion color_conversion; + bool color_conversion_initialized; + gsr_color_conversion *output_color_conversion; + gsr_plugins plugins; + gsr_recording_clock *recording_clock; + gsr_audio_capture audio_capture; + bool audio_capture_initialized; + gsr_replay_save replay_save; + + gsr_recording_output replay_recording_output; + size_t replay_recording_items[GSR_MAX_RECORDING_DESTINATIONS]; + size_t num_replay_recording_items; + char replay_recording_filepath[PATH_MAX]; + bool replay_recording; + + double fps_start_time; + int fps_counter; + int damage_fps_counter; + bool paused; + double record_start_time; + int64_t video_pts_counter; + int64_t video_prev_pts; + bool hdr_metadata_set; + + atomic_int running; + atomic_int toggle_pause; + atomic_int set_paused_request; + atomic_int replay_recording_request; + atomic_int replay_recording_state; + atomic_int save_replay_seconds; + atomic_int save_replay_restart_replay; + bool should_stop_error; + bool force_iframe_frame; + int audio_max_frame_size; + bool use_damage_tracking; + gsr_damage damage; + const char **plugin_filepaths; + int num_plugin_filepaths; +#ifdef GSR_APP_AUDIO + gsr_pipewire_audio *pipewire_audio; +#endif +}; + +static void gsr_recorder_stop_recording(gsr_recorder *self); + +static int recorder_setup_container(gsr_recorder *self) { + // The output format is automatically guessed by the file extension + avformat_alloc_output_context2(&self->av_format_context, NULL, self->settings.container_format, self->settings.filename); + if (!self->av_format_context) { + if(self->settings.container_format) { + gsr_log(GSR_LOG_LEVEL_ERROR, "Container format '%s' (argument -c) is not valid", self->settings.container_format); + } else { + gsr_log(GSR_LOG_LEVEL_ERROR, "Failed to deduce container format from file extension. Use the '-c' option to specify container format"); + args_parser_print_usage(); + return GSR_ERROR_GENERIC; + } + return GSR_ERROR_GENERIC; + } + + set_format_context_options(self->av_format_context); + + const AVOutputFormat *output_format = self->av_format_context->oformat; + + const char *file_extensions = output_format->extensions ? output_format->extensions : ""; + const char *file_extension_end = strchr(file_extensions, ','); + if(file_extension_end) + snprintf(self->file_extension, sizeof(self->file_extension), "%.*s", (int)(file_extension_end - file_extensions), file_extensions); + else + snprintf(self->file_extension, sizeof(self->file_extension), "%s", file_extensions); + + if(self->file_extension[0] == '\0') + snprintf(self->file_extension, sizeof(self->file_extension), "%s", self->settings.container_format ? self->settings.container_format : ""); + + self->force_no_audio_offset = self->settings.is_livestream || self->settings.is_output_piped || (strcmp(self->file_extension, "mp4") != 0 && strcmp(self->file_extension, "mkv") != 0 && strcmp(self->file_extension, "webm") != 0); + self->target_fps = 1.0 / (double)self->settings.fps; + + self->uses_amix = gsr_audio_input_tracks_should_use_amix(self->audio_input_tracks); + self->settings.audio_codec = select_audio_codec_with_fallback(self->settings.audio_codec, self->file_extension, self->uses_amix); + + return GSR_ERROR_OK; +} + +static int recorder_setup_video_sources(gsr_recorder *self) { + self->video_size = (vec2i){0, 0}; + const int video_sources_result = gsr_video_sources_create(&self->video_sources_data, &self->settings, self->egl, self->capture_deps, false, self->capture_sources, &self->video_size); + if(video_sources_result != GSR_ERROR_OK) { + return video_sources_result; + } + + self->video_sources = &self->video_sources_data; + + // (Some?) livestreaming services require at least one audio track to work. + // If not audio is provided then create one silent audio track. + if(self->settings.is_livestream && self->audio_input_tracks->num_items == 0) { + gsr_log(GSR_LOG_LEVEL_INFO, "live streaming but no audio track was added. Adding a silent audio track"); + gsr_merged_audio_inputs silent_audio_track; + memset(&silent_audio_track, 0, sizeof(silent_audio_track)); + gsr_audio_input silent_audio_input; + memset(&silent_audio_input, 0, sizeof(silent_audio_input)); + if(!gsr_merged_audio_inputs_add(&silent_audio_track, &silent_audio_input) || !gsr_audio_input_tracks_add(self->audio_input_tracks, &silent_audio_track)) { + return GSR_ERROR_GENERIC; + } + } + + self->video_stream = NULL; + + return GSR_ERROR_OK; +} + +static int recorder_setup_video_encoder(gsr_recorder *self) { + if(self->settings.video_encoder == GSR_VIDEO_ENCODER_HW_CPU && self->settings.video_codec != (gsr_video_codec)GSR_VIDEO_CODEC_AUTO && self->settings.video_codec != GSR_VIDEO_CODEC_H264) { + gsr_log(GSR_LOG_LEVEL_ERROR, "-encoder cpu was specified but a codec other than h264 was specified. -encoder cpu supports only h264 at the moment"); + return GSR_ERROR_GENERIC; + } + + self->low_power = false; + const AVCodec *video_codec_f = NULL; + const int select_video_codec_result = select_video_codec_with_fallback(self->video_size, &self->settings, self->file_extension, self->egl, &self->low_power, &video_codec_f); + if(select_video_codec_result != GSR_ERROR_OK) { + return select_video_codec_result; + } + + const enum AVPixelFormat video_pix_fmt = get_pixel_format(self->settings.video_codec, self->egl->gpu_info.vendor, self->settings.video_encoder == GSR_VIDEO_ENCODER_HW_CPU); + self->video_codec_context = create_video_codec_context(video_pix_fmt, video_codec_f, self->egl, &self->settings, self->video_size.x, self->video_size.y); + if(!self->settings.is_replaying) + self->video_stream = create_stream(self->av_format_context, self->video_codec_context); + + self->video_frame = av_frame_alloc(); + if(!self->video_frame) { + gsr_log(GSR_LOG_LEVEL_ERROR, "Failed to allocate video frame"); + return GSR_ERROR_GENERIC; + } + self->video_frame->format = self->video_codec_context->pix_fmt; + self->video_frame->width = self->video_size.x; + self->video_frame->height = self->video_size.y; + self->video_frame->color_range = self->video_codec_context->color_range; + self->video_frame->color_primaries = self->video_codec_context->color_primaries; + self->video_frame->color_trc = self->video_codec_context->color_trc; + self->video_frame->colorspace = self->video_codec_context->colorspace; + self->video_frame->chroma_location = self->video_codec_context->chroma_sample_location; + + const size_t estimated_replay_buffer_packets = calculate_estimated_replay_buffer_packets(self->settings.replay_buffer_size_secs, self->settings.fps, self->settings.audio_codec, self->audio_input_tracks); + self->recording_clock = gsr_recording_clock_create(); + if(!self->recording_clock) { + return GSR_ERROR_GENERIC; + } + + self->encoder_initialized = true; + if(!gsr_encoder_init(&self->encoder, self->settings.replay_storage, estimated_replay_buffer_packets, self->settings.replay_buffer_size_secs, self->settings.filename)) { + gsr_log(GSR_LOG_LEVEL_ERROR, "failed to create encoder"); + return GSR_ERROR_GENERIC; + } + + self->video_encoder = create_video_encoder(self->egl, &self->settings); + if(!self->video_encoder) { + gsr_log(GSR_LOG_LEVEL_ERROR, "failed to create video encoder"); + return GSR_ERROR_GENERIC; + } + + if(!gsr_video_encoder_start(self->video_encoder, self->video_codec_context, self->video_frame)) { + gsr_log(GSR_LOG_LEVEL_ERROR, "failed to start video encoder"); + return GSR_ERROR_GENERIC; + } + + self->video_size.x = self->video_codec_context->width; + self->video_size.y = self->video_codec_context->height; + gsr_video_sources_update_with_real_video_size(self->video_sources, self->video_size); + + memset(&self->plugins, 0, sizeof(self->plugins)); + + if(gsr_load_plugins(&self->plugins, self->plugin_filepaths, self->num_plugin_filepaths, &self->settings, self->egl, self->video_size) != GSR_ERROR_OK) { + return GSR_ERROR_GENERIC; + + } + + gsr_color_conversion_params color_conversion_params; + memset(&color_conversion_params, 0, sizeof(color_conversion_params)); + color_conversion_params.color_range = self->settings.color_range; + color_conversion_params.egl = self->egl; + color_conversion_params.load_external_image_shader = gsr_video_sources_uses_external_image(self->video_sources); + gsr_video_encoder_get_textures(self->video_encoder, color_conversion_params.destination_textures, color_conversion_params.destination_textures_size, &color_conversion_params.num_destination_textures, &color_conversion_params.destination_color); + + self->color_conversion_initialized = true; + if(gsr_color_conversion_init(&self->color_conversion, &color_conversion_params) != 0) { + gsr_log(GSR_LOG_LEVEL_ERROR, "main: failed to create color conversion"); + return GSR_ERROR_GENERIC; + } + + gsr_color_conversion_clear(&self->color_conversion); + + self->output_color_conversion = self->plugins.num_plugins > 0 ? &self->plugins.color_conversion : &self->color_conversion; + + if(self->settings.video_encoder == GSR_VIDEO_ENCODER_HW_CPU) { + if(!open_video_software(self->video_codec_context, &self->settings)) { + return GSR_ERROR_GENERIC; + } + } else { + if(!open_video_hardware(self->video_codec_context, self->low_power, self->egl, &self->settings)) { + return GSR_ERROR_GENERIC; + } + } + + if(self->video_stream) { + avcodec_parameters_from_context(self->video_stream->codecpar, self->video_codec_context); + const size_t video_destination_id = gsr_encoder_add_recording_destination(&self->encoder, self->video_codec_context, self->av_format_context, self->video_stream, 0); + if(self->settings.write_first_frame_ts && video_destination_id != (size_t)-1) { + char ts_filepath[PATH_MAX + 4]; + snprintf(ts_filepath, sizeof(ts_filepath), "%s.ts", self->settings.filename); + gsr_encoder_set_recording_destination_first_frame_ts_filepath(&self->encoder, video_destination_id, ts_filepath); + } + } + + return GSR_ERROR_OK; +} + +static int recorder_setup_audio_track(gsr_recorder *self, const gsr_merged_audio_inputs *merged_audio_inputs, int audio_stream_index, gsr_audio_track *audio_track) { + const bool use_amix = gsr_audio_inputs_should_use_amix(merged_audio_inputs); + + memset(audio_track, 0, sizeof(*audio_track)); + audio_track->stream_index = audio_stream_index; + audio_track->codec_context = create_audio_codec_context(self->settings.fps, self->settings.audio_codec, use_amix, self->settings.audio_bitrate); + if(!audio_track->codec_context) + return GSR_ERROR_GENERIC; + + AVStream *audio_stream = NULL; + if(!self->settings.is_replaying) { + audio_stream = create_stream(self->av_format_context, audio_track->codec_context); + if(!audio_stream) + return GSR_ERROR_GENERIC; + + if(gsr_encoder_add_recording_destination(&self->encoder, audio_track->codec_context, self->av_format_context, audio_stream, 0) == (size_t)-1) + gsr_log(GSR_LOG_LEVEL_ERROR, "added too many audio sources"); + } + + snprintf(audio_track->name, sizeof(audio_track->name), "%s", merged_audio_inputs->track_name); + if(audio_stream && audio_track->name[0] != '\0' && !self->settings.exclude_metadata) + av_dict_set(&audio_stream->metadata, "title", audio_track->name, 0); + + if(!open_audio(audio_track->codec_context, self->settings.ffmpeg_audio_opts)) + return GSR_ERROR_GENERIC; + + if(audio_stream) + avcodec_parameters_from_context(audio_stream->codecpar, audio_track->codec_context); + + #if LIBAVCODEC_VERSION_MAJOR < 60 + const int num_channels = audio_track->codec_context->channels; + #else + const int num_channels = audio_track->codec_context->ch_layout.nb_channels; + #endif + + AVFilterContext *src_filter_ctx[GSR_MAX_AUDIO_SOURCES_PER_TRACK]; + if(use_amix) { + if(merged_audio_inputs->num_items > GSR_MAX_AUDIO_SOURCES_PER_TRACK) { + gsr_log(GSR_LOG_LEVEL_ERROR, "too many audio sources for one audio track, the maximum is %d", GSR_MAX_AUDIO_SOURCES_PER_TRACK); + return GSR_ERROR_GENERIC; + } + + if(gsr_audio_init_filter_graph(audio_track->codec_context, &audio_track->graph, &audio_track->sink, src_filter_ctx, merged_audio_inputs->num_items) < 0) { + gsr_log(GSR_LOG_LEVEL_ERROR, "failed to create audio filter"); + return GSR_ERROR_GENERIC; + } + } + + const double audio_fps = (double)audio_track->codec_context->sample_rate / (double)audio_track->codec_context->frame_size; + const double timeout_sec = 1000.0 / audio_fps / 1000.0; + + const double audio_startup_time_seconds = self->force_no_audio_offset ? 0 : audio_codec_get_desired_delay(self->settings.audio_codec, self->settings.fps); + const double num_audio_frames_shift = audio_startup_time_seconds / timeout_sec; + audio_track->pts = -audio_track->codec_context->frame_size * num_audio_frames_shift; + + if(gsr_audio_inputs_has_app_audio(merged_audio_inputs)) { + assert(!use_amix); +#ifdef GSR_APP_AUDIO + return gsr_audio_track_init_application_input(audio_track, merged_audio_inputs, audio_track->codec_context, num_channels, num_audio_frames_shift, self->pipewire_audio); +#else + return GSR_ERROR_UNSUPPORTED; +#endif + } + + return gsr_audio_track_init_device_inputs(audio_track, merged_audio_inputs, audio_track->codec_context, num_channels, num_audio_frames_shift, src_filter_ctx, use_amix); +} + +static int recorder_setup_audio(gsr_recorder *self) { + if(gsr_audio_capture_init(&self->audio_capture, &self->encoder, self->recording_clock, &self->running) != GSR_ERROR_OK) + return GSR_ERROR_GENERIC; + + int audio_stream_index = GSR_VIDEO_STREAM_INDEX + 1; + for(size_t i = 0; i < self->audio_input_tracks->num_items; ++i) { + gsr_audio_track audio_track; + const int audio_track_result = recorder_setup_audio_track(self, &self->audio_input_tracks->items[i], audio_stream_index, &audio_track); + if(audio_track_result != GSR_ERROR_OK) { + gsr_audio_track_deinit(&audio_track); + return audio_track_result; + } + + if(!gsr_audio_capture_add_track(&self->audio_capture, &audio_track)) { + gsr_audio_track_deinit(&audio_track); + return GSR_ERROR_GENERIC; + } + + ++audio_stream_index; + + if(audio_track.codec_context->frame_size > self->audio_max_frame_size) + self->audio_max_frame_size = audio_track.codec_context->frame_size; + } + + return GSR_ERROR_OK; +} + +static int recorder_open_output(gsr_recorder *self) { + if(!self->settings.is_replaying && !(self->av_format_context->oformat->flags & AVFMT_NOFILE)) { + const int ret = avio_open(&self->av_format_context->pb, self->settings.filename, AVIO_FLAG_WRITE); + if(ret < 0) { + gsr_log(GSR_LOG_LEVEL_ERROR, "Could not open '%s': %s", self->settings.filename, gsr_av_error_to_string(ret)); + return GSR_ERROR_GENERIC; + } + } + + if(!self->settings.is_replaying) + av_write_header(self->av_format_context, self->settings.ffmpeg_opts); + + return GSR_ERROR_OK; +} + +gsr_recorder* gsr_recorder_create(const gsr_recorder_params *params, const gsr_recorder_callbacks *callbacks, int *error) { + *error = GSR_ERROR_GENERIC; + + gsr_recorder *self = calloc(1, sizeof(gsr_recorder)); + if(!self) { + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_recorder_create: failed to allocate recorder"); + return NULL; + } + + self->settings = *params->settings; + if(callbacks) + self->callbacks = *callbacks; + self->windowing = params->windowing; + self->egl = ¶ms->windowing->egl; + self->window = params->windowing->window; + self->capture_deps = params->capture_deps; + self->capture_sources = params->capture_sources; + self->audio_input_tracks = params->audio_input_tracks; + atomic_init(&self->running, 1); + atomic_init(&self->toggle_pause, 0); + atomic_init(&self->set_paused_request, GSR_SET_PAUSED_REQUEST_NONE); + atomic_init(&self->replay_recording_request, GSR_REPLAY_RECORDING_REQUEST_NONE); + atomic_init(&self->replay_recording_state, 0); + atomic_init(&self->save_replay_seconds, 0); + atomic_init(&self->save_replay_restart_replay, GSR_RESTART_REPLAY_USE_OPTION); + self->audio_max_frame_size = 1024; + int error_code = GSR_ERROR_GENERIC; + self->hdr = video_codec_is_hdr(params->settings->video_codec); + self->plugin_filepaths = params->plugin_filepaths; + self->num_plugin_filepaths = params->num_plugin_filepaths; +#ifdef GSR_APP_AUDIO + self->pipewire_audio = params->pipewire_audio; +#endif + + const struct { + int (*setup)(gsr_recorder *self); + } setup_phases[] = { + { recorder_setup_container }, + { recorder_setup_video_sources }, + { recorder_setup_video_encoder }, + { recorder_setup_audio }, + { recorder_open_output }, + }; + + for(size_t i = 0; i < sizeof(setup_phases)/sizeof(setup_phases[0]); ++i) { + error_code = setup_phases[i].setup(self); + if(error_code != GSR_ERROR_OK) + goto fail; + } + + *error = GSR_ERROR_OK; + return self; + + fail: + *error = error_code; + gsr_recorder_destroy(self, false); + return NULL; +} + +static void recorder_process_events(gsr_recorder *self) { + while(gsr_window_process_event(self->window)) { + if(self->capture_deps->x11_cursor_display && self->settings.record_cursor) + gsr_cursor_on_event(&self->capture_deps->x11_cursor, gsr_window_get_event_data(self->window)); + + gsr_damage_on_event(&self->damage, gsr_window_get_event_data(self->window)); + for(size_t video_source_index = 0; video_source_index < self->video_sources->num_items; ++video_source_index) { + gsr_video_source *video_source = &self->video_sources->items[video_source_index]; + gsr_capture_on_event(video_source->capture, self->egl); + } + } + + if(self->capture_deps->x11_cursor_display && self->settings.record_cursor) + gsr_cursor_tick(&self->capture_deps->x11_cursor, DefaultRootWindow(self->capture_deps->x11_cursor_display)); +} + +static bool recorder_tick_video_sources(gsr_recorder *self) { + gsr_damage_tick(&self->damage); + + self->should_stop_error = false; + bool damaged = false; + + if(self->use_damage_tracking) + damaged = gsr_damage_is_damaged(&self->damage); + + for(size_t video_source_index = 0; video_source_index < self->video_sources->num_items; ++video_source_index) { + gsr_video_source *video_source = &self->video_sources->items[video_source_index]; + gsr_capture_tick(video_source->capture); + + if(gsr_capture_should_stop(video_source->capture, &self->should_stop_error)) { + atomic_store(&self->running, 0); + break; + } + + if(video_source->capture_source->type == GSR_CAPTURE_SOURCE_TYPE_FOCUSED_WINDOW) { + assert(video_source->capture->get_window_id); + const Window damage_target_window = video_source->capture->get_window_id(video_source->capture); + + if((int64_t)damage_target_window != video_source->capture_source->window_id) { + gsr_damage_stop_tracking_window(&self->damage, video_source->capture_source->window_id); + if(damage_target_window != 0) + gsr_damage_start_tracking_window(&self->damage, damage_target_window); + } + + video_source->capture_source->window_id = damage_target_window; + } + + if(video_source->capture->is_damaged) + damaged |= video_source->capture->is_damaged(video_source->capture); + else if(!self->use_damage_tracking) + damaged = true; + } + + damaged |= gsr_plugins_is_damaged(&self->plugins); + + // TODO: Readd wayland sync warning when removing this + if(self->settings.framerate_mode != GSR_FRAMERATE_MODE_CONTENT) + damaged = true; + + if(damaged) + ++self->damage_fps_counter; + + return damaged; +} + +static void recorder_update_fps_counters(gsr_recorder *self) { + ++self->fps_counter; + const double time_now = clock_get_monotonic_seconds(); + //const double frame_timer_elapsed = time_now - frame_timer_start; + const double elapsed = time_now - self->fps_start_time; + if (elapsed >= 1.0) { + if(self->settings.verbose) { + gsr_log(GSR_LOG_LEVEL_INFO, "update fps: %d, damage fps: %d", self->fps_counter, self->damage_fps_counter); + } + self->fps_start_time = time_now; + self->fps_counter = 0; + self->damage_fps_counter = 0; + } +} + +static void recorder_capture_and_encode_frame(gsr_recorder *self, bool damaged) { + const double this_video_frame_time = gsr_recording_clock_get_time(self->recording_clock); + const int64_t expected_frames = floor((this_video_frame_time - self->record_start_time) / self->target_fps); + const int64_t num_missed_frames = expected_frames - self->video_pts_counter; + + if(damaged && num_missed_frames >= 1 && !self->paused) { + // TODO: Dont do this if no damage? + self->egl->glClear(0); + + gsr_damage_clear(&self->damage); + gsr_plugins_clear_damage(&self->plugins); + gsr_capture_deps_cleanup_kms_fds(self->capture_deps); + + gsr_capture_deps_update_kms(self->capture_deps); + + bool capture_has_synchronous_task = false; + for(size_t video_source_index = 0; video_source_index < self->video_sources->num_items; ++video_source_index) { + gsr_video_source *video_source = &self->video_sources->items[video_source_index]; + if(video_source->capture->clear_damage) + video_source->capture->clear_damage(video_source->capture); + + if(video_source->capture->capture_has_synchronous_task) { + capture_has_synchronous_task = video_source->capture->capture_has_synchronous_task(video_source->capture); + if(capture_has_synchronous_task) { + self->paused = true; + gsr_recording_clock_set_paused(self->recording_clock, true); + } + } + } + + for(size_t video_source_index = 0; video_source_index < self->video_sources->num_items; ++video_source_index) { + gsr_video_source *video_source = &self->video_sources->items[video_source_index]; + if(video_source->capture->pre_capture) + video_source->capture->pre_capture(video_source->capture, &video_source->metadata, self->output_color_conversion); + } + + if(self->output_color_conversion->schedule_clear) { + self->output_color_conversion->schedule_clear = false; + gsr_color_conversion_clear(self->output_color_conversion); + } + + for(size_t video_source_index = 0; video_source_index < self->video_sources->num_items; ++video_source_index) { + gsr_video_source *video_source = &self->video_sources->items[video_source_index]; + gsr_capture_capture(video_source->capture, &video_source->metadata, self->output_color_conversion); + } + + gsr_capture_deps_cleanup_kms_fds(self->capture_deps); + + if(self->plugins.num_plugins > 0) { + gsr_plugins_draw(&self->plugins); + gsr_color_conversion_draw(&self->color_conversion, self->plugins.texture, + (vec2i){0, 0}, self->video_size, + (vec2i){0, 0}, self->video_size, + self->video_size, GSR_ROT_0, GSR_FLIP_NONE, GSR_SOURCE_COLOR_RGB, false); + } + + if(capture_has_synchronous_task) { + self->paused = false; + gsr_recording_clock_set_paused(self->recording_clock, false); + } + + gsr_egl_swap_buffers(self->egl); + gsr_video_encoder_copy_textures_to_frame(self->video_encoder, self->video_frame, self->output_color_conversion); + + for(size_t video_source_index = 0; video_source_index < self->video_sources->num_items; ++video_source_index) { + gsr_video_source *video_source = &self->video_sources->items[video_source_index]; + if(self->hdr && !self->hdr_metadata_set && !self->settings.is_replaying && add_hdr_metadata_to_video_stream(video_source->capture, self->video_stream)) + self->hdr_metadata_set = true; + } + + // TODO: Check if duplicate frame can be saved just by writing it with a different pts instead of sending it again + const int num_frames_to_encode = self->settings.framerate_mode == GSR_FRAMERATE_MODE_CONSTANT ? num_missed_frames : 1; + for(int i = 0; i < num_frames_to_encode; ++i) { + if(self->settings.framerate_mode == GSR_FRAMERATE_MODE_CONSTANT) { + self->video_frame->pts = self->video_pts_counter + i; + } else { + self->video_frame->pts = (this_video_frame_time - self->record_start_time) * (double)AV_TIME_BASE; + const bool same_pts = self->video_frame->pts == self->video_prev_pts; + self->video_prev_pts = self->video_frame->pts; + if(same_pts) + continue; + } + + if(self->force_iframe_frame) { + self->video_frame->pict_type = AV_PICTURE_TYPE_I; + } + + int ret = avcodec_send_frame(self->video_codec_context, self->video_frame); + if(ret == 0) { + // TODO: Move to separate thread because this could write to network (for example when livestreaming) + gsr_encoder_receive_packets(&self->encoder, self->video_codec_context, self->video_frame->pts, GSR_VIDEO_STREAM_INDEX); + } else { + gsr_log(GSR_LOG_LEVEL_ERROR, "avcodec_send_frame failed, error: %s", gsr_av_error_to_string(ret)); + } + + if(self->force_iframe_frame) { + self->force_iframe_frame = false; + self->video_frame->pict_type = AV_PICTURE_TYPE_NONE; + } + } + + self->video_pts_counter += num_missed_frames; + } +} + +static void recorder_apply_pause_toggle(gsr_recorder *self) { + const bool toggle_pause = atomic_exchange(&self->toggle_pause, 0) == 1; + const int set_paused_request = atomic_exchange(&self->set_paused_request, GSR_SET_PAUSED_REQUEST_NONE); + if(self->settings.is_replaying) + return; + + bool new_paused = self->paused; + if(toggle_pause) + new_paused = !new_paused; + if(set_paused_request != GSR_SET_PAUSED_REQUEST_NONE) + new_paused = set_paused_request == GSR_SET_PAUSED_REQUEST_PAUSE; + + if(new_paused != self->paused) { + self->paused = new_paused; + gsr_recording_clock_set_paused(self->recording_clock, self->paused); + gsr_log(GSR_LOG_LEVEL_INFO, self->paused ? "Paused" : "Unpaused"); + } +} + +static void recorder_apply_replay_recording_toggle(gsr_recorder *self) { + const int request = atomic_exchange(&self->replay_recording_request, GSR_REPLAY_RECORDING_REQUEST_NONE); + if(request == GSR_REPLAY_RECORDING_REQUEST_NONE) + return; + + if(!self->settings.replay_recording_directory) { + if(request != GSR_REPLAY_RECORDING_REQUEST_STOP && self->callbacks.recording_started) + self->callbacks.recording_started(NULL, self->callbacks.userdata); + return; + } + + bool new_replay_recording_state = !self->replay_recording; + if(request == GSR_REPLAY_RECORDING_REQUEST_START) + new_replay_recording_state = true; + else if(request == GSR_REPLAY_RECORDING_REQUEST_STOP) + new_replay_recording_state = false; + + if(new_replay_recording_state == self->replay_recording) + return; + + if(new_replay_recording_state) { + gsr_audio_capture_lock_filter(&self->audio_capture); + self->num_replay_recording_items = 0; + const bool filepath_created = gsr_create_new_recording_filepath_from_timestamp(self->replay_recording_filepath, sizeof(self->replay_recording_filepath), self->settings.replay_recording_directory, "Video", self->file_extension, self->settings.date_folders); + if(filepath_created && gsr_recording_output_start(&self->replay_recording_output, self->replay_recording_filepath, &self->settings, self->video_codec_context, &self->audio_capture, self->hdr, self->video_sources)) { + const size_t video_recording_destination_id = gsr_encoder_add_recording_destination(&self->encoder, self->video_codec_context, self->replay_recording_output.av_format_context, self->replay_recording_output.video_stream, self->video_frame->pts); + if(self->settings.write_first_frame_ts && video_recording_destination_id != (size_t)-1) { + char ts_filepath[PATH_MAX + 4]; + snprintf(ts_filepath, sizeof(ts_filepath), "%s.ts", self->replay_recording_filepath); + gsr_encoder_set_recording_destination_first_frame_ts_filepath(&self->encoder, video_recording_destination_id, ts_filepath); + } + + if(video_recording_destination_id != (size_t)-1 && self->num_replay_recording_items < GSR_MAX_RECORDING_DESTINATIONS) { + self->replay_recording_items[self->num_replay_recording_items] = video_recording_destination_id; + ++self->num_replay_recording_items; + } + + for(size_t i = 0; i < self->replay_recording_output.num_audio_streams; ++i) { + const gsr_recording_audio_stream *audio_stream = &self->replay_recording_output.audio_streams[i]; + const size_t audio_recording_destination_id = gsr_encoder_add_recording_destination(&self->encoder, audio_stream->audio_track->codec_context, self->replay_recording_output.av_format_context, audio_stream->stream, audio_stream->audio_track->pts); + if(audio_recording_destination_id != (size_t)-1 && self->num_replay_recording_items < GSR_MAX_RECORDING_DESTINATIONS) { + self->replay_recording_items[self->num_replay_recording_items] = audio_recording_destination_id; + ++self->num_replay_recording_items; + } + } + + self->replay_recording = true; + atomic_store(&self->replay_recording_state, 1); + self->force_iframe_frame = true; + gsr_log(GSR_LOG_LEVEL_INFO, "Started recording"); + if(self->callbacks.recording_started) + self->callbacks.recording_started(self->replay_recording_filepath, self->callbacks.userdata); + } else { + if(self->callbacks.recording_started) + self->callbacks.recording_started(NULL, self->callbacks.userdata); + } + gsr_audio_capture_unlock_filter(&self->audio_capture); + } else if(self->replay_recording_output.av_format_context) { + for(size_t i = 0; i < self->num_replay_recording_items; ++i) { + gsr_encoder_remove_recording_destination(&self->encoder, self->replay_recording_items[i]); + } + self->num_replay_recording_items = 0; + + if(gsr_recording_output_stop(&self->replay_recording_output)) { + gsr_log(GSR_LOG_LEVEL_INFO, "Stopped recording"); + if(self->callbacks.recording_stopped) + self->callbacks.recording_stopped(self->replay_recording_filepath, self->callbacks.userdata); + } else { + if(self->callbacks.recording_stopped) + self->callbacks.recording_stopped(NULL, self->callbacks.userdata); + } + + self->replay_recording = false; + atomic_store(&self->replay_recording_state, 0); + self->replay_recording_filepath[0] = '\0'; + } +} + +static void recorder_poll_replay_save(gsr_recorder *self) { + bool replay_save_result = false; + const char *replay_save_output_filepath = NULL; + if(gsr_replay_save_poll(&self->replay_save, &replay_save_result, &replay_save_output_filepath)) { + if(self->callbacks.replay_saved) + self->callbacks.replay_saved(replay_save_output_filepath[0] == '\0' || !replay_save_result ? NULL : replay_save_output_filepath, self->callbacks.userdata); + } + + if(atomic_load(&self->save_replay_seconds) != 0 && !gsr_replay_save_is_running(&self->replay_save) && self->settings.is_replaying) { + int current_save_replay_seconds = atomic_load(&self->save_replay_seconds); + if(current_save_replay_seconds > 0) + current_save_replay_seconds += self->settings.keyint; + + atomic_store(&self->save_replay_seconds, 0); + const int restart_replay_request = atomic_exchange(&self->save_replay_restart_replay, GSR_RESTART_REPLAY_USE_OPTION); + const bool restart_replay = restart_replay_request == GSR_RESTART_REPLAY_USE_OPTION ? self->settings.restart_replay_on_save : restart_replay_request == GSR_RESTART_REPLAY_ENABLE; + const bool replay_start_result = gsr_replay_save_start(&self->replay_save, self->video_codec_context, GSR_VIDEO_STREAM_INDEX, &self->audio_capture, &self->encoder, &self->settings, self->file_extension, self->hdr, self->video_sources, current_save_replay_seconds); + if(!replay_start_result && self->callbacks.replay_saved) + self->callbacks.replay_saved(NULL, self->callbacks.userdata); + + if(restart_replay && current_save_replay_seconds == GSR_SAVE_REPLAY_SECONDS_FULL) { + pthread_mutex_lock(&self->encoder.replay_mutex); + gsr_replay_buffer_clear(self->encoder.replay_buffer); + pthread_mutex_unlock(&self->encoder.replay_mutex); + } + } +} + +static void recorder_sleep_until_next_frame(gsr_recorder *self) { + const double time_at_frame_end = gsr_recording_clock_get_time(self->recording_clock); + const double time_elapsed_total = time_at_frame_end - self->record_start_time; + const int64_t frames_elapsed = floor(time_elapsed_total / self->target_fps); + const double time_at_next_frame = (frames_elapsed + 1) * self->target_fps; + double time_to_next_frame = time_at_next_frame - time_elapsed_total; + if(time_to_next_frame > self->target_fps) + time_to_next_frame = self->target_fps; + const int64_t end_num_missed_frames = frames_elapsed - self->video_pts_counter; + + if(time_to_next_frame > 0.0 && end_num_missed_frames <= 0) + av_usleep(time_to_next_frame * 1000.0 * 1000.0); + else { + if(self->paused) + av_usleep(20.0 * 1000.0); // 20 milliseconds + else if(self->settings.framerate_mode == GSR_FRAMERATE_MODE_CONTENT) + av_usleep(2.8 * 1000.0); // 2.8 milliseconds + } +} + +int gsr_recorder_run(gsr_recorder *self) { + self->fps_start_time = clock_get_monotonic_seconds(); + //double frame_timer_start = self->fps_start_time; + self->fps_counter = 0; + self->damage_fps_counter = 0; + + self->paused = false; + self->replay_recording = false; + + memset(&self->replay_recording_output, 0, sizeof(self->replay_recording_output)); + + gsr_replay_save_init(&self->replay_save); + + self->force_iframe_frame = false; + + gsr_recording_clock_start(self->recording_clock); + self->record_start_time = gsr_recording_clock_get_start_time(self->recording_clock); + + if(gsr_audio_capture_start(&self->audio_capture, self->audio_max_frame_size, self->uses_amix) != GSR_ERROR_OK) { + /* The audio threads that did start have to stop before they can be joined */ + atomic_store(&self->running, 0); + return GSR_ERROR_GENERIC; + } + + // Set update_fps to 24 to test if duplicate/delayed frames cause video/audio desync or too fast/slow video. + //const double update_fps = fps + 190; + self->should_stop_error = false; + + self->video_pts_counter = 0; + self->video_prev_pts = 0; + + self->hdr_metadata_set = false; + self->hdr = video_codec_is_hdr(self->settings.video_codec); + + memset(&self->damage, 0, sizeof(self->damage)); + if(self->settings.framerate_mode == GSR_FRAMERATE_MODE_CONTENT && gsr_capture_sources_has_damage_tracked_target(self->capture_sources)) { + if(gsr_window_get_display_server(self->window) == GSR_DISPLAY_SERVER_X11) { + gsr_damage_init(&self->damage, self->egl, &self->capture_deps->x11_cursor, self->settings.record_cursor); + self->use_damage_tracking = true; + + for(size_t i = 0; i < self->capture_sources->num_items; ++i) { + const gsr_capture_source *capture_source = &self->capture_sources->items[i]; + switch(capture_source->type) { + case GSR_CAPTURE_SOURCE_TYPE_WINDOW: + gsr_damage_start_tracking_window(&self->damage, capture_source->window_id); + break; + case GSR_CAPTURE_SOURCE_TYPE_MONITOR: + case GSR_CAPTURE_SOURCE_TYPE_REGION: + // TODO: When capturing a region only track damage in that region + gsr_damage_start_tracking_monitor(&self->damage, capture_source->name); + break; + default: + break; + } + } + } else if(gsr_capture_sources_has_monitor_or_region(self->capture_sources)) { + gsr_log(GSR_LOG_LEVEL_WARNING, "\"-fm content\" has no effect on Wayland when recording a monitor. Either record a monitor on X11 or capture with desktop portal instead (-w portal)"); + } + } + + while(atomic_load(&self->running)) { + recorder_process_events(self); + const bool damaged = recorder_tick_video_sources(self); + recorder_update_fps_counters(self); + recorder_capture_and_encode_frame(self, damaged); + recorder_apply_pause_toggle(self); + recorder_apply_replay_recording_toggle(self); + recorder_poll_replay_save(self); + recorder_sleep_until_next_frame(self); + } + + gsr_recorder_stop_recording(self); + return self->should_stop_error ? GSR_ERROR_CAPTURE_FAILED : GSR_ERROR_OK; +} + +static void gsr_recorder_stop_recording(gsr_recorder *self) { + atomic_store(&self->running, 0); + + bool final_replay_save_result = false; + const char *final_replay_save_output_filepath = NULL; + if(gsr_replay_save_join(&self->replay_save, &final_replay_save_result, &final_replay_save_output_filepath)) { + if(final_replay_save_output_filepath[0] != '\0' && self->callbacks.replay_saved) + self->callbacks.replay_saved(final_replay_save_output_filepath, self->callbacks.userdata); + } + + gsr_plugins_deinit(&self->plugins); + + if(self->replay_recording_output.av_format_context) { + for(size_t i = 0; i < self->num_replay_recording_items; ++i) { + gsr_encoder_remove_recording_destination(&self->encoder, self->replay_recording_items[i]); + } + self->num_replay_recording_items = 0; + + if(gsr_recording_output_stop(&self->replay_recording_output)) { + gsr_log(GSR_LOG_LEVEL_INFO, "Stopped recording"); + if(self->callbacks.recording_stopped) + self->callbacks.recording_stopped(self->replay_recording_filepath, self->callbacks.userdata); + } else { + if(self->callbacks.recording_stopped) + self->callbacks.recording_stopped(NULL, self->callbacks.userdata); + } + } + + gsr_audio_capture_join_threads(&self->audio_capture); + + // TODO: Replace this with start_recording_create_steams + if(!self->settings.is_replaying && gsr_av_format_context_write_trailer(self->av_format_context) != 0) { + //fprintf(stderr, "Failed to write trailer\n"); + } + + if(!self->settings.is_replaying && self->callbacks.recording_stopped) + self->callbacks.recording_stopped(self->settings.filename, self->callbacks.userdata); +} + +void gsr_recorder_destroy(gsr_recorder *self, bool exiting) { + if(!self) + return; + + gsr_audio_capture_deinit(&self->audio_capture); + gsr_plugins_deinit(&self->plugins); + + if(self->use_damage_tracking) + gsr_damage_deinit(&self->damage); + + if(self->color_conversion_initialized) + gsr_color_conversion_deinit(&self->color_conversion); + + if(self->video_frame) + av_frame_free(&self->video_frame); + + if(self->video_codec_context) + avcodec_free_context(&self->video_codec_context); + + if(self->video_encoder) + gsr_video_encoder_destroy(self->video_encoder, NULL); + + if(self->encoder_initialized) + gsr_encoder_deinit(&self->encoder, exiting); + + gsr_video_sources_deinit(&self->video_sources_data); + + if(self->av_format_context) { + if(self->av_format_context->pb && !(self->av_format_context->oformat->flags & AVFMT_NOFILE)) + avio_close(self->av_format_context->pb); + avformat_free_context(self->av_format_context); + } + + gsr_recording_clock_destroy(self->recording_clock); + free(self); +} + +void gsr_recorder_stop(gsr_recorder *self) { + atomic_store(&self->running, 0); +} + +void gsr_recorder_toggle_pause(gsr_recorder *self) { + atomic_store(&self->toggle_pause, 1); +} + +void gsr_recorder_set_paused(gsr_recorder *self, bool paused) { + atomic_store(&self->set_paused_request, paused ? GSR_SET_PAUSED_REQUEST_PAUSE : GSR_SET_PAUSED_REQUEST_UNPAUSE); +} + +void gsr_recorder_toggle_replay_recording(gsr_recorder *self) { + atomic_store(&self->replay_recording_request, GSR_REPLAY_RECORDING_REQUEST_TOGGLE); +} + +void gsr_recorder_start_replay_recording(gsr_recorder *self) { + atomic_store(&self->replay_recording_request, GSR_REPLAY_RECORDING_REQUEST_START); +} + +void gsr_recorder_stop_replay_recording(gsr_recorder *self) { + atomic_store(&self->replay_recording_request, GSR_REPLAY_RECORDING_REQUEST_STOP); +} + +bool gsr_recorder_is_replay_recording(const gsr_recorder *self) { + return atomic_load(&self->replay_recording_state) == 1; +} + +void gsr_recorder_save_replay(gsr_recorder *self, int seconds, int restart_replay) { + atomic_store(&self->save_replay_restart_replay, restart_replay); + atomic_store(&self->save_replay_seconds, seconds); +} diff --git a/src/recorder/recording_clock.c b/src/recorder/recording_clock.c new file mode 100644 index 0000000..5c9aabb --- /dev/null +++ b/src/recorder/recording_clock.c @@ -0,0 +1,60 @@ +#include "../../include/recorder/recording_clock.h" +#include "../../include/utils.h" +#include "../../include/log.h" + +#include <stdlib.h> +#include <stdatomic.h> + +struct gsr_recording_clock { + double record_start_time; + _Atomic double paused_time_offset; + double paused_time_start; + atomic_bool paused; +}; + +gsr_recording_clock* gsr_recording_clock_create(void) { + gsr_recording_clock *self = calloc(1, sizeof(gsr_recording_clock)); + if(!self) { + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_recording_clock_create: failed to allocate recording clock"); + return NULL; + } + + atomic_init(&self->paused_time_offset, 0.0); + atomic_init(&self->paused, false); + self->record_start_time = clock_get_monotonic_seconds(); + return self; +} + +void gsr_recording_clock_destroy(gsr_recording_clock *self) { + if(self) + free(self); +} + +void gsr_recording_clock_start(gsr_recording_clock *self) { + self->record_start_time = clock_get_monotonic_seconds(); +} + +double gsr_recording_clock_get_start_time(const gsr_recording_clock *self) { + return self->record_start_time; +} + +double gsr_recording_clock_get_time(const gsr_recording_clock *self) { + return clock_get_monotonic_seconds() - atomic_load(&self->paused_time_offset); +} + +void gsr_recording_clock_set_paused(gsr_recording_clock *self, bool paused) { + if(paused == atomic_load(&self->paused)) + return; + + if(paused) { + self->paused_time_start = clock_get_monotonic_seconds(); + } else { + atomic_store(&self->paused_time_offset, atomic_load(&self->paused_time_offset) + (clock_get_monotonic_seconds() - self->paused_time_start)); + } + + atomic_store(&self->paused, paused); +} + +bool gsr_recording_clock_is_paused(const gsr_recording_clock *self) { + return atomic_load(&self->paused); +} diff --git a/src/recorder/replay_save.c b/src/recorder/replay_save.c new file mode 100644 index 0000000..3251616 --- /dev/null +++ b/src/recorder/replay_save.c @@ -0,0 +1,209 @@ +#include "../../include/recorder/replay_save.h" +#include "../../include/ffmpeg_utils.h" +#include "../../include/log.h" + +#include <string.h> +#include <stdlib.h> +#include <assert.h> + +void gsr_replay_save_init(gsr_replay_save *self) { + memset(self, 0, sizeof(*self)); + atomic_init(&self->finished, 0); +} + +bool gsr_replay_save_is_running(const gsr_replay_save *self) { + return self->thread_created; +} + +static void gsr_replay_save_cleanup(gsr_replay_save *self) { + if(self->cloned_replay_buffer) { + pthread_mutex_lock(&self->encoder->replay_mutex); + gsr_replay_buffer_destroy(self->cloned_replay_buffer); + pthread_mutex_unlock(&self->encoder->replay_mutex); + self->cloned_replay_buffer = NULL; + } + + if(self->audio_pts_offsets) { + free(self->audio_pts_offsets); + self->audio_pts_offsets = NULL; + } + self->num_audio_pts_offsets = 0; +} + +static void* replay_save_thread(void *userdata) { + gsr_replay_save *self = userdata; + bool success = true; + gsr_replay_buffer_iterator replay_iterator = self->video_start_iterator; + + for(;;) { + AVPacket *replay_packet = gsr_replay_buffer_iterator_get_packet(self->cloned_replay_buffer, replay_iterator); + uint8_t *replay_packet_data = NULL; + if(replay_packet) { + pthread_mutex_lock(&self->encoder->replay_mutex); + replay_packet_data = gsr_replay_buffer_iterator_get_packet_data(self->cloned_replay_buffer, replay_iterator); + pthread_mutex_unlock(&self->encoder->replay_mutex); + } + + if(!replay_packet) { + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_replay_save: no replay packet"); + success = false; + break; + } + + if(!replay_packet->data && !replay_packet_data) { + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_replay_save: no replay packet data"); + success = false; + break; + } + + // TODO: Check if successful + AVPacket av_packet; + memset(&av_packet, 0, sizeof(av_packet)); + //av_packet_from_data(av_packet, replay_packet->data, replay_packet->size); + av_packet.data = replay_packet->data ? replay_packet->data : replay_packet_data; + av_packet.size = replay_packet->size; + av_packet.stream_index = replay_packet->stream_index; + av_packet.pts = replay_packet->pts; + av_packet.dts = replay_packet->pts; + av_packet.flags = replay_packet->flags; + //av_packet.duration = replay_packet->duration; + + AVStream *stream = self->recording_output.video_stream; + AVCodecContext *codec_context = self->video_codec_context; + + if(av_packet.stream_index == self->video_stream_index) { + av_packet.pts -= self->video_pts_offset; + av_packet.dts -= self->video_pts_offset; + } else { + gsr_recording_audio_stream *recording_start_audio = gsr_recording_output_get_audio_stream_by_index(&self->recording_output, av_packet.stream_index); + if(!recording_start_audio) { + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_replay_save: failed to find audio stream by index: %d", av_packet.stream_index); + free(replay_packet_data); + continue; + } + + const gsr_audio_track *audio_track = recording_start_audio->audio_track; + stream = recording_start_audio->stream; + codec_context = audio_track->codec_context; + + const gsr_audio_pts_offset *audio_pts_offset = &self->audio_pts_offsets[av_packet.stream_index - 1]; + assert(audio_pts_offset->stream_index == av_packet.stream_index); + av_packet.pts -= audio_pts_offset->pts_offset; + av_packet.dts -= audio_pts_offset->pts_offset; + } + + //av_packet.stream_index = stream->index; + av_packet_rescale_ts(&av_packet, codec_context->time_base, stream->time_base); + + const int ret = av_write_frame(self->recording_output.av_format_context, &av_packet); + if(ret >= 0) + gsr_av_format_context_mark_packet_written(self->recording_output.av_format_context); + else + gsr_log(GSR_LOG_LEVEL_ERROR, "Failed to write frame index %d to muxer, reason: %s (%d)", av_packet.stream_index, gsr_av_error_to_string(ret), ret); + + free(replay_packet_data); + + //av_packet_free(&av_packet); + if(!gsr_replay_buffer_iterator_next(self->cloned_replay_buffer, &replay_iterator)) + break; + } + + gsr_recording_output_stop(&self->recording_output); + + self->success = success; + gsr_replay_save_cleanup(self); + atomic_store(&self->finished, 1); + return NULL; +} + +bool gsr_replay_save_start(gsr_replay_save *self, AVCodecContext *video_codec_context, int video_stream_index, const gsr_audio_capture *audio_capture, gsr_encoder *encoder, const gsr_recorder_settings *settings, const char *file_extension, bool hdr, gsr_video_sources *video_sources, int current_save_replay_seconds) { + if(self->thread_created) + return true; + + self->encoder = encoder; + self->video_codec_context = video_codec_context; + self->video_stream_index = video_stream_index; + self->output_filepath[0] = '\0'; + self->success = false; + atomic_store(&self->finished, 0); + + pthread_mutex_lock(&encoder->replay_mutex); + self->cloned_replay_buffer = gsr_replay_buffer_clone(encoder->replay_buffer); + pthread_mutex_unlock(&encoder->replay_mutex); + if(!self->cloned_replay_buffer) { + /* TODO: Return this error to mark the replay as failed */ + gsr_log(GSR_LOG_LEVEL_ERROR, "failed to save replay: failed to clone replay buffer"); + return false; + } + + const gsr_replay_buffer_iterator start_iterator = {0, 0}; + const gsr_replay_buffer_iterator search_start_iterator = current_save_replay_seconds == GSR_SAVE_REPLAY_SECONDS_FULL ? start_iterator : gsr_replay_buffer_find_packet_index_by_time_passed(self->cloned_replay_buffer, current_save_replay_seconds); + self->video_start_iterator = gsr_replay_buffer_find_keyframe(self->cloned_replay_buffer, search_start_iterator, video_stream_index, false); + if(self->video_start_iterator.packet_index == (size_t)-1) { + gsr_log(GSR_LOG_LEVEL_ERROR, "failed to save replay: failed to find a video keyframe. perhaps replay was saved too fast, before anything has been recorded"); + gsr_replay_save_cleanup(self); + return false; + } + + self->video_pts_offset = gsr_replay_buffer_iterator_get_packet(self->cloned_replay_buffer, self->video_start_iterator)->pts; + + if(audio_capture->num_tracks > 0) { + self->audio_pts_offsets = calloc(audio_capture->num_tracks, sizeof(gsr_audio_pts_offset)); + if(!self->audio_pts_offsets) { + gsr_log(GSR_LOG_LEVEL_ERROR, "failed to save replay: failed to allocate audio pts offsets"); + gsr_replay_save_cleanup(self); + return false; + } + } + + for(size_t i = 0; i < audio_capture->num_tracks; ++i) { + const gsr_audio_track *audio_track = &audio_capture->tracks[i]; + const gsr_replay_buffer_iterator audio_start_iterator = gsr_replay_buffer_find_keyframe(self->cloned_replay_buffer, self->video_start_iterator, audio_track->stream_index, false); + const int64_t audio_pts_offset = audio_start_iterator.packet_index == (size_t)-1 ? 0 : gsr_replay_buffer_iterator_get_packet(self->cloned_replay_buffer, audio_start_iterator)->pts; + self->audio_pts_offsets[i].pts_offset = audio_pts_offset; + self->audio_pts_offsets[i].stream_index = audio_track->stream_index; + ++self->num_audio_pts_offsets; + } + + if(!gsr_create_new_recording_filepath_from_timestamp(self->output_filepath, sizeof(self->output_filepath), settings->filename, "Replay", file_extension, settings->date_folders)) { + gsr_replay_save_cleanup(self); + return false; + } + + if(!gsr_recording_output_start(&self->recording_output, self->output_filepath, settings, video_codec_context, audio_capture, hdr, video_sources)) { + gsr_replay_save_cleanup(self); + return false; + } + + if(pthread_create(&self->thread, NULL, replay_save_thread, self) != 0) { + gsr_log(GSR_LOG_LEVEL_ERROR, "failed to save replay: failed to create thread"); + gsr_recording_output_stop(&self->recording_output); + gsr_replay_save_cleanup(self); + return false; + } + + self->thread_created = true; + return true; +} + +static bool gsr_replay_save_finish(gsr_replay_save *self, bool *success, const char **output_filepath) { + pthread_join(self->thread, NULL); + self->thread_created = false; + *success = self->success; + *output_filepath = self->output_filepath; + return true; +} + +bool gsr_replay_save_poll(gsr_replay_save *self, bool *success, const char **output_filepath) { + if(!self->thread_created || !atomic_load(&self->finished)) + return false; + + return gsr_replay_save_finish(self, success, output_filepath); +} + +bool gsr_replay_save_join(gsr_replay_save *self, bool *success, const char **output_filepath) { + if(!self->thread_created) + return false; + + return gsr_replay_save_finish(self, success, output_filepath); +} diff --git a/src/recorder/screenshot.c b/src/recorder/screenshot.c new file mode 100644 index 0000000..2e4357d --- /dev/null +++ b/src/recorder/screenshot.c @@ -0,0 +1,221 @@ +#include "../../include/recorder/screenshot.h" +#include "../../include/recorder/error.h" +#include "../../include/color_conversion.h" +#include "../../include/window/window.h" +#include "../../include/log.h" + +#include <string.h> +#include <assert.h> +#include <unistd.h> + +#include <X11/Xlib.h> + +#define JPEG_YUV444_QUALITY_THRESHOLD 91 + +gsr_color_range image_format_to_color_range(gsr_image_format image_format, int image_quality) { + switch(image_format) { + case GSR_IMAGE_FORMAT_JPEG: return image_quality >= JPEG_YUV444_QUALITY_THRESHOLD ? GSR_COLOR_RANGE_FULL : GSR_COLOR_RANGE_LIMITED; + case GSR_IMAGE_FORMAT_PNG: return GSR_COLOR_RANGE_FULL; + } + assert(false); + return GSR_COLOR_RANGE_FULL; +} + +int video_quality_to_image_quality_value(gsr_video_quality video_quality) { + switch(video_quality) { + case GSR_VIDEO_QUALITY_MEDIUM: + return 75; + case GSR_VIDEO_QUALITY_HIGH: + return 85; + case GSR_VIDEO_QUALITY_VERY_HIGH: + return JPEG_YUV444_QUALITY_THRESHOLD; // Quality above 90 makes the jpeg image encoder (stb_image_writer) use yuv444 instead of yuv420, which greatly improves small colored text quality on dark background + case GSR_VIDEO_QUALITY_ULTRA: + return 97; + } + assert(false); + return 90; +} + +int gsr_load_plugins(gsr_plugins *plugins, const char **plugin_filepaths, int num_plugin_filepaths, const gsr_recorder_settings *settings, gsr_egl *egl, vec2i video_size) { + if(num_plugin_filepaths == 0) + return GSR_ERROR_OK; + + const gsr_color_depth color_depth = video_codec_to_bit_depth(settings->video_codec); + assert(color_depth == GSR_COLOR_DEPTH_8_BITS || color_depth == GSR_COLOR_DEPTH_10_BITS); + + gsr_plugin_init_params plugin_init_params; + plugin_init_params.width = video_size.x; + plugin_init_params.height = video_size.y; + plugin_init_params.fps = settings->fps; + plugin_init_params.color_depth = color_depth == GSR_COLOR_DEPTH_8_BITS ? GSR_PLUGIN_COLOR_DEPTH_8_BITS : GSR_PLUGIN_COLOR_DEPTH_10_BITS; + plugin_init_params.graphics_api = egl->context_type == GSR_GL_CONTEXT_TYPE_GLX ? GSR_PLUGIN_GRAPHICS_API_GLX : GSR_PLUGIN_GRAPHICS_API_EGL_ES; + + if(!gsr_plugins_init(plugins, plugin_init_params, egl)) + return GSR_ERROR_GENERIC; + + for(int i = 0; i < num_plugin_filepaths; ++i) { + if(!gsr_plugins_load_plugin(plugins, plugin_filepaths[i])) + return GSR_ERROR_GENERIC; + } + + return GSR_ERROR_OK; +} + +int gsr_screenshot_take(const gsr_screenshot_params *params) { + const gsr_recorder_settings *settings = params->settings; + gsr_egl *egl = params->egl; + gsr_window *window = params->window; + gsr_capture_deps *capture_deps = params->capture_deps; + gsr_capture_sources *capture_sources = params->capture_sources; + const atomic_int *running = params->running; + const int image_quality = video_quality_to_image_quality_value(settings->video_quality); + const gsr_color_range color_range = image_format_to_color_range(params->image_format, image_quality); + + vec2i video_size = {0, 0}; + gsr_video_sources video_sources_data; + const int video_sources_result = gsr_video_sources_create(&video_sources_data, settings, egl, capture_deps, true, capture_sources, &video_size); + if(video_sources_result != GSR_ERROR_OK) + return video_sources_result; + gsr_video_sources *video_sources = &video_sources_data; + gsr_video_sources_update_with_real_video_size(video_sources, video_size); + + gsr_plugins plugins; + memset(&plugins, 0, sizeof(plugins)); + + const int load_plugins_result = gsr_load_plugins(&plugins, params->plugin_filepaths, params->num_plugin_filepaths, settings, egl, video_size); + if(load_plugins_result != GSR_ERROR_OK) { + gsr_video_sources_deinit(video_sources); + return load_plugins_result; + } + + int result = GSR_ERROR_GENERIC; + gsr_image_writer image_writer; + memset(&image_writer, 0, sizeof(image_writer)); + gsr_color_conversion color_conversion; + memset(&color_conversion, 0, sizeof(color_conversion)); + + if(!gsr_image_writer_init_opengl(&image_writer, egl, video_size.x, video_size.y)) { + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_screenshot_take: gsr_image_write_gl_init failed"); + goto done; + } + + gsr_color_conversion_params color_conversion_params; + memset(&color_conversion_params, 0, sizeof(color_conversion_params)); + color_conversion_params.color_range = color_range; + color_conversion_params.egl = egl; + color_conversion_params.load_external_image_shader = gsr_video_sources_uses_external_image(video_sources); + + color_conversion_params.destination_textures[0] = image_writer.texture; + color_conversion_params.destination_textures_size[0] = video_size; + color_conversion_params.num_destination_textures = 1; + color_conversion_params.destination_color = GSR_DESTINATION_COLOR_RGB; + + if(gsr_color_conversion_init(&color_conversion, &color_conversion_params) != 0) { + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_screenshot_take: failed to create color conversion"); + goto done; + } + + gsr_color_conversion_clear(&color_conversion); + + gsr_color_conversion *output_color_conversion = plugins.num_plugins > 0 ? &plugins.color_conversion : &color_conversion; + + bool should_stop_error = false; + egl->glClear(0); + + while(atomic_load(running)) { + while(gsr_window_process_event(window)) { + if(capture_deps->x11_cursor_display && settings->record_cursor) + gsr_cursor_on_event(&capture_deps->x11_cursor, gsr_window_get_event_data(window)); + + for(size_t video_source_index = 0; video_source_index < video_sources->num_items; ++video_source_index) { + gsr_video_source *video_source = &video_sources->items[video_source_index]; + gsr_capture_on_event(video_source->capture, egl); + } + } + + if(capture_deps->x11_cursor_display && settings->record_cursor) + gsr_cursor_tick(&capture_deps->x11_cursor, DefaultRootWindow(capture_deps->x11_cursor_display)); + + gsr_capture_deps_cleanup_kms_fds(capture_deps); + + gsr_capture_deps_update_kms(capture_deps); + + should_stop_error = false; + for(size_t video_source_index = 0; video_source_index < video_sources->num_items; ++video_source_index) { + gsr_video_source *video_source = &video_sources->items[video_source_index]; + gsr_capture_tick(video_source->capture); + if(gsr_capture_should_stop(video_source->capture, &should_stop_error)) { + break; + break; + } + } + + for(size_t video_source_index = 0; video_source_index < video_sources->num_items; ++video_source_index) { + gsr_video_source *video_source = &video_sources->items[video_source_index]; + if(video_source->capture->pre_capture) + video_source->capture->pre_capture(video_source->capture, &video_source->metadata, output_color_conversion); + } + + if(output_color_conversion->schedule_clear) { + output_color_conversion->schedule_clear = false; + gsr_color_conversion_clear(output_color_conversion); + } + + bool all_sources_captured = true; + for(size_t video_source_index = 0; video_source_index < video_sources->num_items; ++video_source_index) { + gsr_video_source *video_source = &video_sources->items[video_source_index]; + // It can fail, for example when capturing portal and the target is a monitor that hasn't been updated. + // This can also happen for example if the system suspends and the monitor to capture's framebuffer is gone, or if the target window disappeared. + if(gsr_capture_capture(video_source->capture, &video_source->metadata, output_color_conversion) != 0) + all_sources_captured = false; + } + + gsr_capture_deps_cleanup_kms_fds(capture_deps); + + if(all_sources_captured) + break; + + if(atomic_load(running)) + usleep(30 * 1000); // 30 ms + } + + if(plugins.num_plugins > 0) { + gsr_plugins_draw(&plugins); + gsr_color_conversion_draw(&color_conversion, plugins.texture, + (vec2i){0, 0}, video_size, + (vec2i){0, 0}, video_size, + video_size, GSR_ROT_0, GSR_FLIP_NONE, GSR_SOURCE_COLOR_RGB, false); + } + + gsr_egl_swap_buffers(egl); + + result = should_stop_error ? GSR_ERROR_CAPTURE_FAILED : GSR_ERROR_OK; + if(!should_stop_error) { + if(!gsr_image_writer_write_to_file(&image_writer, settings->filename, params->image_format, image_quality)) { + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_screenshot_take: failed to write opengl texture to image output file %s", settings->filename); + result = GSR_ERROR_GENERIC; + } + + if(result == GSR_ERROR_OK && params->screenshot_saved) + params->screenshot_saved(settings->filename, params->userdata); + } + + done: + gsr_color_conversion_deinit(&color_conversion); + gsr_plugins_deinit(&plugins); + gsr_image_writer_deinit(&image_writer); + gsr_video_sources_deinit(video_sources); + return result; +} + +bool get_image_format_from_filename(const char *filename, gsr_image_format *image_format) { + if(gsr_string_ends_with(filename, ".jpg") || gsr_string_ends_with(filename, ".jpeg")) { + *image_format = GSR_IMAGE_FORMAT_JPEG; + return true; + } else if(gsr_string_ends_with(filename, ".png")) { + *image_format = GSR_IMAGE_FORMAT_PNG; + return true; + } else { + return false; + } +} diff --git a/src/recorder/video_codec.c b/src/recorder/video_codec.c new file mode 100644 index 0000000..a21e4ec --- /dev/null +++ b/src/recorder/video_codec.c @@ -0,0 +1,428 @@ +#include "../../include/recorder/video_codec.h" +#include "../../include/ffmpeg_utils.h" +#include "../../include/log.h" + +#include <assert.h> + +#include <libavutil/opt.h> + +int video_quality_to_h264_equivalent_qp(gsr_video_quality video_quality) { + switch(video_quality) { + case GSR_VIDEO_QUALITY_MEDIUM: return 35; + case GSR_VIDEO_QUALITY_HIGH: return 30; + case GSR_VIDEO_QUALITY_VERY_HIGH: return 25; + case GSR_VIDEO_QUALITY_ULTRA: return 22; + } + return 22; +} + +static int video_quality_to_codec_quality_value(enum AVCodecID codec_id, gsr_video_quality video_quality) { + const int h264_qp = video_quality_to_h264_equivalent_qp(video_quality); + switch(codec_id) { + case AV_CODEC_ID_H264: + case AV_CODEC_ID_HEVC: + return h264_qp; + case AV_CODEC_ID_AV1: + case AV_CODEC_ID_VP9: + return h264_qp * 4; + case AV_CODEC_ID_VP8: + return h264_qp * 2; + default: + return h264_qp; + } +} + +static int vbr_get_quality_parameter(AVCodecContext *codec_context, gsr_video_quality video_quality, bool hdr) { + // 8 bit / 10 bit = 80% + const float qp_multiply = hdr ? 8.0f/10.0f : 1.0f; + return video_quality_to_codec_quality_value(codec_context->codec_id, video_quality) * qp_multiply; +} + +AVCodecContext *create_video_codec_context(enum AVPixelFormat pix_fmt, const AVCodec *codec, const gsr_egl *egl, const gsr_recorder_settings *settings, int width, int height) { + const bool use_software_video_encoder = settings->video_encoder == GSR_VIDEO_ENCODER_HW_CPU; + const bool hdr = video_codec_is_hdr(settings->video_codec); + AVCodecContext *codec_context = avcodec_alloc_context3(codec); + + //double fps_ratio = (double)fps / 30.0; + + assert(codec->type == AVMEDIA_TYPE_VIDEO); + codec_context->codec_id = codec->id; + codec_context->width = width; + codec_context->height = height; + // Timebase: This is the fundamental unit of time (in seconds) in terms + // of which frame timestamps are represented. For fixed-fps content, + // timebase should be 1/framerate and timestamp increments should be + // identical to 1 + codec_context->time_base.num = 1; + codec_context->time_base.den = settings->framerate_mode == GSR_FRAMERATE_MODE_CONSTANT ? settings->fps : AV_TIME_BASE; + codec_context->framerate.num = settings->fps; + codec_context->framerate.den = 1; + codec_context->sample_aspect_ratio.num = 0; + codec_context->sample_aspect_ratio.den = 0; + if(settings->low_latency_recording) { + codec_context->flags |= (AV_CODEC_FLAG_CLOSED_GOP | AV_CODEC_FLAG_LOW_DELAY); + codec_context->flags2 |= AV_CODEC_FLAG2_FAST; + //codec_context->gop_size = std::numeric_limits<int>::max(); + //codec_context->keyint_min = std::numeric_limits<int>::max(); + codec_context->gop_size = settings->fps * settings->keyint; + } else { + // High values reduce file size but increases time it takes to seek + codec_context->gop_size = settings->fps * settings->keyint; + } + codec_context->max_b_frames = 0; + codec_context->pix_fmt = pix_fmt; + codec_context->color_range = settings->color_range == GSR_COLOR_RANGE_LIMITED ? AVCOL_RANGE_MPEG : AVCOL_RANGE_JPEG; + if(hdr) { + codec_context->color_primaries = AVCOL_PRI_BT2020; + codec_context->color_trc = AVCOL_TRC_SMPTE2084; + codec_context->colorspace = AVCOL_SPC_BT2020_NCL; + } else { + codec_context->color_primaries = AVCOL_PRI_BT709; + codec_context->color_trc = AVCOL_TRC_BT709; + codec_context->colorspace = AVCOL_SPC_BT709; + } + //codec_context->chroma_sample_location = AVCHROMA_LOC_CENTER; + // Can't use this because it's fucking broken in ffmpeg 8 or new mesa. It produces garbage output + //if(codec->id == AV_CODEC_ID_HEVC) + // codec_context->codec_tag = MKTAG('h', 'v', 'c', '1'); // QuickTime on MacOS requires this or the video wont be playable + + if(settings->bitrate_mode == GSR_BITRATE_MODE_CBR) { + codec_context->bit_rate = settings->video_bitrate; + codec_context->rc_max_rate = codec_context->bit_rate; + //codec_context->rc_min_rate = codec_context->bit_rate; + codec_context->rc_buffer_size = codec_context->bit_rate;//codec_context->bit_rate / 10; + codec_context->rc_initial_buffer_occupancy = 0;//codec_context->bit_rate;//codec_context->bit_rate * 1000; + } else if(settings->bitrate_mode == GSR_BITRATE_MODE_VBR) { + const int quality = vbr_get_quality_parameter(codec_context, settings->video_quality, hdr); + switch(settings->video_quality) { + case GSR_VIDEO_QUALITY_MEDIUM: + codec_context->qmin = quality; + codec_context->qmax = quality; + codec_context->bit_rate = 100000;//4500000 + (codec_context->width * codec_context->height)*0.75; + break; + case GSR_VIDEO_QUALITY_HIGH: + codec_context->qmin = quality; + codec_context->qmax = quality; + codec_context->bit_rate = 100000;//10000000-9000000 + (codec_context->width * codec_context->height)*0.75; + break; + case GSR_VIDEO_QUALITY_VERY_HIGH: + codec_context->qmin = quality; + codec_context->qmax = quality; + codec_context->bit_rate = 100000;//10000000-9000000 + (codec_context->width * codec_context->height)*0.75; + break; + case GSR_VIDEO_QUALITY_ULTRA: + codec_context->qmin = quality; + codec_context->qmax = quality; + codec_context->bit_rate = 100000;//10000000-9000000 + (codec_context->width * codec_context->height)*0.75; + break; + } + + codec_context->rc_max_rate = codec_context->bit_rate; + //codec_context->rc_min_rate = codec_context->bit_rate; + codec_context->rc_buffer_size = codec_context->bit_rate;//codec_context->bit_rate / 10; + codec_context->rc_initial_buffer_occupancy = codec_context->bit_rate;//codec_context->bit_rate * 1000; + } else { + //codec_context->rc_buffer_size = 50000 * 1000; + } + //codec_context->profile = FF_PROFILE_H264_MAIN; + if (codec_context->codec_id == AV_CODEC_ID_MPEG1VIDEO) + codec_context->mb_decision = 2; + + const bool uses_vaapi_encoder = !use_software_video_encoder && egl->gpu_info.vendor != GSR_GPU_VENDOR_NVIDIA && !video_codec_is_vulkan(settings->video_codec); + if(uses_vaapi_encoder && settings->bitrate_mode != GSR_BITRATE_MODE_CBR) { + // 8 bit / 10 bit = 80%, and increase it even more + const float quality_multiply = hdr ? (8.0f/10.0f * 0.7f) : 1.0f; + codec_context->global_quality = video_quality_to_codec_quality_value(codec_context->codec_id, settings->video_quality) * quality_multiply; + } + + av_opt_set_int(codec_context->priv_data, "b_ref_mode", 0, 0); + //av_opt_set_int(codec_context->priv_data, "cbr", true, 0); + + if(egl->gpu_info.vendor != GSR_GPU_VENDOR_NVIDIA || video_codec_is_vulkan(settings->video_codec)) { + // TODO: More options, better options + //codec_context->bit_rate = codec_context->width * codec_context->height; + switch(settings->bitrate_mode) { + case GSR_BITRATE_MODE_QP: { + if(video_codec_is_vulkan(settings->video_codec)) + av_opt_set(codec_context->priv_data, "rc_mode", "cqp", 0); + else if(egl->gpu_info.vendor == GSR_GPU_VENDOR_NVIDIA) + av_opt_set(codec_context->priv_data, "rc", "constqp", 0); + else + av_opt_set(codec_context->priv_data, "rc_mode", "CQP", 0); + break; + } + case GSR_BITRATE_MODE_VBR: { + if(video_codec_is_vulkan(settings->video_codec)) + av_opt_set(codec_context->priv_data, "rc_mode", "vbr", 0); + else if(egl->gpu_info.vendor == GSR_GPU_VENDOR_NVIDIA) + av_opt_set(codec_context->priv_data, "rc", "vbr", 0); + else + av_opt_set(codec_context->priv_data, "rc_mode", "VBR", 0); + break; + } + case GSR_BITRATE_MODE_CBR: { + if(video_codec_is_vulkan(settings->video_codec)) + av_opt_set(codec_context->priv_data, "rc_mode", "cbr", 0); + else if(egl->gpu_info.vendor == GSR_GPU_VENDOR_NVIDIA) + av_opt_set(codec_context->priv_data, "rc", "cbr", 0); + else + av_opt_set(codec_context->priv_data, "rc_mode", "CBR", 0); + break; + } + } + //codec_context->global_quality = 4; + //codec_context->compression_level = 2; + } + + //av_opt_set(codec_context->priv_data, "bsf", "hevc_metadata=colour_primaries=9:transfer_characteristics=16:matrix_coefficients=9", 0); + + if(settings->tune == GSR_TUNE_QUALITY) + codec_context->max_b_frames = 2; + + codec_context->flags |= AV_CODEC_FLAG_GLOBAL_HEADER; + + return codec_context; +} + +static void dict_set_profile(AVCodecContext *codec_context, gsr_gpu_vendor vendor, gsr_color_depth color_depth, gsr_video_codec video_codec, AVDictionary **options) { + #if LIBAVCODEC_VERSION_INT < AV_VERSION_INT(61, 17, 100) + if(codec_context->codec_id == AV_CODEC_ID_H264) { + // TODO: Only for vaapi + //if(color_depth == GSR_COLOR_DEPTH_10_BITS) + // av_dict_set(options, "profile", "high10", 0); + //else + av_dict_set(options, "profile", "high", 0); + } else if(codec_context->codec_id == AV_CODEC_ID_AV1) { + if(vendor == GSR_GPU_VENDOR_NVIDIA) { + if(color_depth == GSR_COLOR_DEPTH_10_BITS) + av_dict_set_int(options, "highbitdepth", 1, 0); + } else { + av_dict_set(options, "profile", "main", 0); // TODO: use professional instead? + } + } else if(codec_context->codec_id == AV_CODEC_ID_HEVC) { + if(color_depth == GSR_COLOR_DEPTH_10_BITS) + av_dict_set(options, "profile", "main10", 0); + else + av_dict_set(options, "profile", "main", 0); + } + #else + const bool use_nvidia_values = vendor == GSR_GPU_VENDOR_NVIDIA && !video_codec_is_vulkan(video_codec); + if(codec_context->codec_id == AV_CODEC_ID_H264) { + // TODO: Only for vaapi + //if(color_depth == GSR_COLOR_DEPTH_10_BITS) + // av_dict_set_int(options, "profile", AV_PROFILE_H264_HIGH_10, 0); + //else + av_dict_set_int(options, "profile", use_nvidia_values ? 2 : AV_PROFILE_H264_HIGH, 0); + } else if(codec_context->codec_id == AV_CODEC_ID_AV1) { + if(use_nvidia_values) { + if(color_depth == GSR_COLOR_DEPTH_10_BITS) + av_dict_set_int(options, "highbitdepth", 1, 0); + } else { + av_dict_set_int(options, "profile", AV_PROFILE_AV1_MAIN, 0); // TODO: use professional instead? + } + } else if(codec_context->codec_id == AV_CODEC_ID_HEVC) { + if(color_depth == GSR_COLOR_DEPTH_10_BITS) + av_dict_set_int(options, "profile", use_nvidia_values ? 1 : AV_PROFILE_HEVC_MAIN_10, 0); + else + av_dict_set_int(options, "profile", use_nvidia_values ? 0 : AV_PROFILE_HEVC_MAIN, 0); + } + #endif +} + +static void video_software_set_qp(AVCodecContext *codec_context, gsr_video_quality video_quality, bool hdr, AVDictionary **options) { + // 8 bit / 10 bit = 80% + const float qp_multiply = hdr ? 8.0f/10.0f : 1.0f; + av_dict_set_int(options, "qp", video_quality_to_codec_quality_value(codec_context->codec_id, video_quality) * qp_multiply, 0); +} + +bool open_video_software(AVCodecContext *codec_context, const gsr_recorder_settings *settings) { + const bool hdr = video_codec_is_hdr(settings->video_codec); + AVDictionary *options = NULL; + + if(settings->bitrate_mode == GSR_BITRATE_MODE_QP) + video_software_set_qp(codec_context, settings->video_quality, hdr, &options); + + av_dict_set(&options, "preset", "veryfast", 0); + av_dict_set(&options, "tune", "film", 0); + av_dict_set_int(&options, "forced-idr", 1, 0); + + if(codec_context->codec_id == AV_CODEC_ID_H264) { + av_dict_set(&options, "coder", "cabac", 0); // TODO: cavlc is faster than cabac but worse compression. Which to use? + } + + av_dict_set(&options, "strict", "experimental", 0); + + if(settings->ffmpeg_video_opts) + av_dict_parse_string(&options, settings->ffmpeg_video_opts, "=", ";", 0); + + int ret = avcodec_open2(codec_context, codec_context->codec, &options); + if (ret < 0) { + gsr_log(GSR_LOG_LEVEL_ERROR, "Could not open video codec: %s", gsr_av_error_to_string(ret)); + return false; + } + + return true; +} + +static void video_set_rc(gsr_video_codec video_codec, gsr_gpu_vendor vendor, gsr_bitrate_mode bitrate_mode, AVDictionary **options) { + switch(bitrate_mode) { + case GSR_BITRATE_MODE_QP: { + if(video_codec_is_vulkan(video_codec)) + av_dict_set(options, "rc_mode", "cqp", 0); + else if(vendor == GSR_GPU_VENDOR_NVIDIA) + av_dict_set(options, "rc", "constqp", 0); + else + av_dict_set(options, "rc_mode", "CQP", 0); + break; + } + case GSR_BITRATE_MODE_VBR: { + if(video_codec_is_vulkan(video_codec)) + av_dict_set(options, "rc_mode", "vbr", 0); + else if(vendor == GSR_GPU_VENDOR_NVIDIA) + av_dict_set(options, "rc", "vbr", 0); + else + av_dict_set(options, "rc_mode", "VBR", 0); + break; + } + case GSR_BITRATE_MODE_CBR: { + if(video_codec_is_vulkan(video_codec)) + av_dict_set(options, "rc_mode", "cbr", 0); + else if(vendor == GSR_GPU_VENDOR_NVIDIA) + av_dict_set(options, "rc", "cbr", 0); + else + av_dict_set(options, "rc_mode", "CBR", 0); + break; + } + } +} + +static void video_hardware_set_qp(AVCodecContext *codec_context, gsr_video_quality video_quality, bool hdr, AVDictionary **options) { + // 8 bit / 10 bit = 80% + const float qp_multiply = hdr ? 8.0f/10.0f : 1.0f; + av_dict_set_int(options, "qp", video_quality_to_codec_quality_value(codec_context->codec_id, video_quality) * qp_multiply, 0); +} + +bool open_video_hardware(AVCodecContext *codec_context, bool low_power, const gsr_egl *egl, const gsr_recorder_settings *settings) { + const gsr_color_depth color_depth = video_codec_to_bit_depth(settings->video_codec); + const bool hdr = video_codec_is_hdr(settings->video_codec); + AVDictionary *options = NULL; + + if(settings->bitrate_mode == GSR_BITRATE_MODE_QP) + video_hardware_set_qp(codec_context, settings->video_quality, hdr, &options); + + video_set_rc(settings->video_codec, egl->gpu_info.vendor, settings->bitrate_mode, &options); + + // TODO: Enable multipass + + dict_set_profile(codec_context, egl->gpu_info.vendor, color_depth, settings->video_codec, &options); + + if(video_codec_is_vulkan(settings->video_codec)) { + av_dict_set_int(&options, "async_depth", 3, 0); + av_dict_set(&options, "tune", "ll", 0); // Low latency + av_dict_set(&options, "usage", settings->is_livestream ? "stream" : "record", 0); + av_dict_set(&options, "content", "rendered", 0); // Game or 3D content + + if(codec_context->codec_id == AV_CODEC_ID_H264) { + // Removed because it causes stutter in games for some people + //av_dict_set_int(&options, "quality", 5, 0); // quality preset + } else if(codec_context->codec_id == AV_CODEC_ID_AV1) { + av_dict_set(&options, "tier", "main", 0); + } else if(codec_context->codec_id == AV_CODEC_ID_HEVC) { + if(hdr) + av_dict_set(&options, "sei", "hdr", 0); + } + } else if(egl->gpu_info.vendor == GSR_GPU_VENDOR_NVIDIA) { + // TODO: These dont seem to be necessary + // av_dict_set_int(&options, "zerolatency", 1, 0); + // if(codec_context->codec_id == AV_CODEC_ID_AV1) { + // av_dict_set(&options, "tune", "ll", 0); + // } else if(codec_context->codec_id == AV_CODEC_ID_H264 || codec_context->codec_id == AV_CODEC_ID_HEVC) { + // av_dict_set(&options, "preset", "llhq", 0); + // av_dict_set(&options, "tune", "ll", 0); + // } + av_dict_set(&options, "tune", "ll", 0); + av_dict_set_int(&options, "forced-idr", 1, 0); + + switch(settings->tune) { + case GSR_TUNE_PERFORMANCE: + //av_dict_set(&options, "multipass", "qres", 0); + break; + case GSR_TUNE_QUALITY: + av_dict_set(&options, "multipass", "fullres", 0); + av_dict_set(&options, "preset", "p6", 0); + av_dict_set_int(&options, "rc-lookahead", 0, 0); + break; + } + + if(codec_context->codec_id == AV_CODEC_ID_H264) { + // TODO: h264 10bit? + // TODO: + // switch(pixel_format) { + // case GSR_PIXEL_FORMAT_YUV420: + // av_dict_set_int(&options, "profile", AV_PROFILE_H264_HIGH, 0); + // break; + // case GSR_PIXEL_FORMAT_YUV444: + // av_dict_set_int(&options, "profile", AV_PROFILE_H264_HIGH_444, 0); + // break; + // } + } else if(codec_context->codec_id == AV_CODEC_ID_AV1) { + switch(settings->pixel_format) { + case GSR_PIXEL_FORMAT_YUV420: + av_dict_set(&options, "rgb_mode", "yuv420", 0); + break; + case GSR_PIXEL_FORMAT_YUV444: + av_dict_set(&options, "rgb_mode", "yuv444", 0); + break; + } + } else if(codec_context->codec_id == AV_CODEC_ID_HEVC) { + //av_dict_set(&options, "pix_fmt", "yuv420p16le", 0); + } + } else { + // TODO: More quality options + if(low_power) + av_dict_set_int(&options, "low_power", 1, 0); + // Improves performance but increases vram. + // TODO: Might need a different async_depth for optimal performance on different amd/intel gpus + av_dict_set_int(&options, "async_depth", 3, 0); + + if(codec_context->codec_id == AV_CODEC_ID_H264) { + // Removed because it causes stutter in games for some people + //av_dict_set_int(&options, "quality", 5, 0); // quality preset + } else if(codec_context->codec_id == AV_CODEC_ID_AV1) { + av_dict_set(&options, "tier", "main", 0); + } else if(codec_context->codec_id == AV_CODEC_ID_HEVC) { + if(hdr) + av_dict_set(&options, "sei", "hdr", 0); + } + + // TODO: vp8/vp9 10bit + } + + if(codec_context->codec_id == AV_CODEC_ID_H264) { + av_dict_set(&options, "coder", "cabac", 0); // TODO: cavlc is faster than cabac but worse compression. Which to use? + } + + av_dict_set(&options, "strict", "experimental", 0); + + if(settings->ffmpeg_video_opts) + av_dict_parse_string(&options, settings->ffmpeg_video_opts, "=", ";", 0); + + int ret = avcodec_open2(codec_context, codec_context->codec, &options); + if (ret < 0) { + gsr_log(GSR_LOG_LEVEL_ERROR, "Could not open video codec: %s", gsr_av_error_to_string(ret)); + return false; + } + + return true; +} + +enum AVPixelFormat get_pixel_format(gsr_video_codec video_codec, gsr_gpu_vendor vendor, bool use_software_video_encoder) { + if(use_software_video_encoder) { + return AV_PIX_FMT_NV12; + } else { + if(video_codec_is_vulkan(video_codec)) + return AV_PIX_FMT_VULKAN; + else + return vendor == GSR_GPU_VENDOR_NVIDIA ? AV_PIX_FMT_CUDA : AV_PIX_FMT_VAAPI; + } +} diff --git a/src/recorder/windowing.c b/src/recorder/windowing.c new file mode 100644 index 0000000..0bd1de6 --- /dev/null +++ b/src/recorder/windowing.c @@ -0,0 +1,140 @@ +#include "../../include/recorder/windowing.h" +#include "../../include/recorder/error.h" +#include "../../include/window/x11.h" +#include "../../include/window/wayland.h" +#include "../../include/utils.h" +#include "../../include/log.h" + +#include <string.h> +#include <stdlib.h> + +static int x11_error_handler(Display *display, XErrorEvent *event) { + (void)display; + (void)event; + return 0; +} + +static int x11_io_error_handler(Display *display) { + (void)display; + return 0; +} + +static void xwayland_check_callback(const gsr_monitor *monitor, void *userdata) { + bool *xwayland_found = (bool*)userdata; + if(monitor->name_len >= 8 && strncmp(monitor->name, "XWAYLAND", 8) == 0) + *xwayland_found = true; + else if(memmem(monitor->name, monitor->name_len, "X11", 3)) + *xwayland_found = true; +} + +static bool is_xwayland(Display *display) { + int opcode, event, error; + if(XQueryExtension(display, "XWAYLAND", &opcode, &event, &error)) + return true; + + bool xwayland_found = false; + for_each_active_monitor_output_x11_not_cached(display, xwayland_check_callback, &xwayland_found); + return xwayland_found; +} + +bool gsr_windowing_is_using_prime_run(void) { + const char *prime_render_offload = getenv("__NV_PRIME_RENDER_OFFLOAD"); + return (prime_render_offload && strcmp(prime_render_offload, "1") == 0) || getenv("DRI_PRIME"); +} + +void gsr_windowing_disable_prime_run(void) { + unsetenv("__NV_PRIME_RENDER_OFFLOAD"); + unsetenv("__NV_PRIME_RENDER_OFFLOAD_PROVIDER"); + unsetenv("__GLX_VENDOR_LIBRARY_NAME"); + unsetenv("__VK_LAYER_NV_optimus"); + unsetenv("DRI_PRIME"); +} + +static gsr_window* window_create(Display *display, bool wayland) { + if(wayland) + return gsr_window_wayland_create(); + else + return gsr_window_x11_create(display); +} + +bool monitor_capture_use_drm(const gsr_window *window, gsr_gpu_vendor vendor) { + return gsr_window_get_display_server(window) == GSR_DISPLAY_SERVER_WAYLAND || vendor != GSR_GPU_VENDOR_NVIDIA; +} + +int gsr_windowing_init(gsr_windowing *self, const gsr_windowing_params *params) { + memset(self, 0, sizeof(*self)); + self->card_path_found = true; + + bool wayland = false; + self->display = XOpenDisplay(NULL); + if(self->display) { + if(params->listen_to_x11_events) + XSelectInput(self->display, DefaultRootWindow(self->display), PropertyChangeMask); + } else { + wayland = true; + gsr_log(GSR_LOG_LEVEL_WARNING, "failed to connect to the X server. Assuming wayland is running without Xwayland"); + } + + XSetErrorHandler(x11_error_handler); + XSetIOErrorHandler(x11_io_error_handler); + + if(!wayland) + wayland = is_xwayland(self->display); + + if(!wayland && gsr_windowing_is_using_prime_run()) { + // Disable prime-run and similar options as it doesn't work, the monitor to capture has to be run on the same device. + // This is fine on wayland since nvidia uses drm interface there and the monitor query checks the monitors connected + // to the drm device. + gsr_log(GSR_LOG_LEVEL_WARNING, "use of prime-run on X11 is not supported. Disabling prime-run"); + gsr_windowing_disable_prime_run(); + } + + self->window = window_create(self->display, wayland); + if(!self->window) { + gsr_log(GSR_LOG_LEVEL_ERROR, "failed to create window"); + return GSR_ERROR_GENERIC; + } + + return GSR_ERROR_OK; +} + +int gsr_windowing_load_egl(gsr_windowing *self, const gsr_windowing_params *params) { + if(!gsr_egl_load(&self->egl, self->window, params->monitor_capture, params->gl_debug)) { + gsr_log(GSR_LOG_LEVEL_ERROR, "failed to load opengl"); + return GSR_ERROR_OPENGL_LOAD_FAILED; + } + self->egl_loaded = true; + + self->egl.card_path[0] = '\0'; + if(monitor_capture_use_drm(self->window, self->egl.gpu_info.vendor)) { + // TODO: Allow specifying another card, and in other places + if(!gsr_get_valid_card_path(&self->egl, self->egl.card_path, params->monitor_capture)) + self->card_path_found = false; + } else { + gsr_get_valid_card_path(&self->egl, self->egl.card_path, false); + } + + return GSR_ERROR_OK; +} + +void gsr_windowing_deinit(gsr_windowing *self) { + if(self->egl_loaded) { + gsr_egl_unload(&self->egl); + self->egl_loaded = false; + } + + if(self->window) { + gsr_window_destroy(self->window); + self->window = NULL; + } + + if(self->display) { + /* TODO: XCloseDisplay causes a crash, why? maybe some other library dlclose xlib and that also happened to unload this??? */ + //XCloseDisplay(self->display); + self->display = NULL; + } +} + +bool gsr_windowing_is_wayland(const gsr_windowing *self) { + return gsr_window_get_display_server(self->window) == GSR_DISPLAY_SERVER_WAYLAND; +} diff --git a/src/replay_buffer/replay_buffer.c b/src/replay_buffer/replay_buffer.c index 56549ee..2f7db54 100644 --- a/src/replay_buffer/replay_buffer.c +++ b/src/replay_buffer/replay_buffer.c @@ -3,8 +3,6 @@ #include "../../include/replay_buffer/replay_buffer_disk.h" #include <stdlib.h> -#include <string.h> -#include <assert.h> gsr_replay_buffer* gsr_replay_buffer_create(gsr_replay_storage replay_storage, const char *replay_directory, double replay_buffer_time, size_t replay_buffer_num_packets) { gsr_replay_buffer *replay_buffer = NULL; @@ -24,6 +22,11 @@ void gsr_replay_buffer_destroy(gsr_replay_buffer *self) { free(self); } +void gsr_replay_buffer_destroy_at_exit(gsr_replay_buffer *self) { + self->destroy_at_exit(self); + /* |self| is intentionally not free'd, the operating system does that when the process exits */ +} + bool gsr_replay_buffer_append(gsr_replay_buffer *self, const AVPacket *av_packet, double timestamp) { return self->append(self, av_packet, timestamp); } diff --git a/src/replay_buffer/replay_buffer_disk.c b/src/replay_buffer/replay_buffer_disk.c index ce42d93..b61ccfa 100644 --- a/src/replay_buffer/replay_buffer_disk.c +++ b/src/replay_buffer/replay_buffer_disk.c @@ -1,8 +1,8 @@ #include "../../include/replay_buffer/replay_buffer_disk.h" +#include "../../include/log.h" #include "../../include/utils.h" #include <stdlib.h> -#include <string.h> #include <stdio.h> #include <fcntl.h> #include <unistd.h> @@ -25,12 +25,12 @@ static void gsr_av_packet_disk_init(gsr_av_packet_disk *self, const AVPacket *av static gsr_replay_buffer_file* gsr_replay_buffer_file_create(char *replay_directory, size_t replay_storage_counter, double timestamp, int *replay_storage_fd) { gsr_replay_buffer_file *self = calloc(1, sizeof(gsr_replay_buffer_file)); if(!self) { - fprintf(stderr, "gsr error: gsr_av_packet_file_init: failed to create buffer file\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_av_packet_file_init: failed to create buffer file"); return NULL; } if(create_directory_recursive(replay_directory) != 0) { - fprintf(stderr, "gsr error: gsr_av_packet_file_init: failed to create replay directory: %s\n", replay_directory); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_av_packet_file_init: failed to create replay directory: %s", replay_directory); free(self); return NULL; } @@ -39,7 +39,7 @@ static gsr_replay_buffer_file* gsr_replay_buffer_file_create(char *replay_direct snprintf(filename, sizeof(filename), "%s/%s_%d.gsr", replay_directory, FILE_PREFIX, (int)replay_storage_counter); *replay_storage_fd = creat(filename, 0700); if(*replay_storage_fd <= 0) { - fprintf(stderr, "gsr error: gsr_av_packet_file_init: failed to create replay file: %s\n", filename); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_av_packet_file_init: failed to create replay file: %s", filename); free(self); return NULL; } @@ -135,7 +135,7 @@ static bool file_write_all(int fd, const uint8_t *data, size_t size, size_t *byt static bool gsr_replay_buffer_disk_create_next_file(gsr_replay_buffer_disk *self, double timestamp) { if(self->num_files + 1 >= GSR_REPLAY_BUFFER_CAPACITY_NUM_FILES) { - fprintf(stderr, "gsr error: gsr_replay_buffer_disk_create_next_file: too many replay buffer files created! (> %d), either reduce the replay buffer time or report this as a bug\n", (int)GSR_REPLAY_BUFFER_CAPACITY_NUM_FILES); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_replay_buffer_disk_create_next_file: too many replay buffer files created! (> %d), either reduce the replay buffer time or report this as a bug", (int)GSR_REPLAY_BUFFER_CAPACITY_NUM_FILES); return false; } @@ -160,7 +160,7 @@ static bool gsr_replay_buffer_disk_append_to_current_file(gsr_replay_buffer_disk void *new_packets = realloc(replay_buffer_file->packets, new_capacity_num_packets * sizeof(gsr_av_packet_disk)); if(!new_packets) { - fprintf(stderr, "gsr error: gsr_replay_buffer_disk_append_to_current_file: failed to reallocate replay buffer file packets\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_replay_buffer_disk_append_to_current_file: failed to reallocate replay buffer file packets"); return false; } @@ -233,20 +233,20 @@ static uint8_t* gsr_replay_buffer_disk_iterator_get_packet_data(gsr_replay_buffe snprintf(filename, sizeof(filename), "%s/%s_%d.gsr", self->replay_directory, FILE_PREFIX, (int)file->id); file->fd = open(filename, O_RDONLY); if(file->fd <= 0) { - fprintf(stderr, "gsr error: gsr_replay_buffer_disk_iterator_get_packet_data: failed to open file\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_replay_buffer_disk_iterator_get_packet_data: failed to open file"); return NULL; } } const gsr_av_packet_disk *packet = &self->files[iterator.file_index]->packets[iterator.packet_index]; if(lseek(file->fd, packet->data_index, SEEK_SET) == -1) { - fprintf(stderr, "gsr error: gsr_replay_buffer_disk_iterator_get_packet_data: failed to seek\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_replay_buffer_disk_iterator_get_packet_data: failed to seek"); return NULL; } uint8_t *packet_data = malloc(packet->packet.size); if(read(file->fd, packet_data, packet->packet.size) != packet->packet.size) { - fprintf(stderr, "gsr error: gsr_replay_buffer_disk_iterator_get_packet_data: failed to read data from file\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_replay_buffer_disk_iterator_get_packet_data: failed to read data from file"); free(packet_data); return NULL; } @@ -393,6 +393,8 @@ static void get_current_time(char *time_str, size_t time_str_size) { static void gsr_replay_buffer_disk_set_impl_funcs(gsr_replay_buffer_disk *self) { self->replay_buffer.destroy = gsr_replay_buffer_disk_destroy; + /* The files and the directory have to be removed even when the process exits */ + self->replay_buffer.destroy_at_exit = gsr_replay_buffer_disk_destroy; self->replay_buffer.append = gsr_replay_buffer_disk_append; self->replay_buffer.clear = gsr_replay_buffer_disk_clear; self->replay_buffer.iterator_get_packet = gsr_replay_buffer_disk_iterator_get_packet; diff --git a/src/replay_buffer/replay_buffer_ram.c b/src/replay_buffer/replay_buffer_ram.c index c5df328..a148a20 100644 --- a/src/replay_buffer/replay_buffer_ram.c +++ b/src/replay_buffer/replay_buffer_ram.c @@ -2,7 +2,6 @@ #include "../../include/utils.h" #include <stdlib.h> -#include <string.h> #include <assert.h> #include <libavutil/mem.h> @@ -71,6 +70,11 @@ static void gsr_replay_buffer_ram_destroy(gsr_replay_buffer *replay_buffer) { self->index = 0; } +/* The replay buffer only holds memory, which the operating system frees when the process exits */ +static void gsr_replay_buffer_ram_destroy_at_exit(gsr_replay_buffer *replay_buffer) { + (void)replay_buffer; +} + static bool gsr_replay_buffer_ram_append(gsr_replay_buffer *replay_buffer, const AVPacket *av_packet, double timestamp) { gsr_replay_buffer_ram *self = (gsr_replay_buffer_ram*)replay_buffer; gsr_av_packet_ram *packet = gsr_av_packet_ram_create(av_packet, timestamp); @@ -91,14 +95,14 @@ static bool gsr_replay_buffer_ram_append(gsr_replay_buffer *replay_buffer, const return true; } +/* + The packets are not free'd here because that takes seconds when the replay buffer holds several gigabytes of data, + which would block the thread that appends to the replay buffer for that long. The packets are instead left in place + where they are no longer reachable and gsr_replay_buffer_ram_append free's them one by one as it overwrites them, + which it already does when the ring buffer wraps around. +*/ static void gsr_replay_buffer_ram_clear(gsr_replay_buffer *replay_buffer) { gsr_replay_buffer_ram *self = (gsr_replay_buffer_ram*)replay_buffer; - for(size_t i = 0; i < self->num_packets; ++i) { - if(self->packets[i]) { - gsr_av_packet_ram_unref(self->packets[i]); - self->packets[i] = NULL; - } - } self->num_packets = 0; self->index = 0; } @@ -206,6 +210,7 @@ static bool gsr_replay_buffer_ram_iterator_next(gsr_replay_buffer *replay_buffer static void gsr_replay_buffer_ram_set_impl_funcs(gsr_replay_buffer_ram *self) { self->replay_buffer.destroy = gsr_replay_buffer_ram_destroy; + self->replay_buffer.destroy_at_exit = gsr_replay_buffer_ram_destroy_at_exit; self->replay_buffer.append = gsr_replay_buffer_ram_append; self->replay_buffer.clear = gsr_replay_buffer_ram_clear; self->replay_buffer.iterator_get_packet = gsr_replay_buffer_ram_iterator_get_packet; diff --git a/src/shader.c b/src/shader.c index f20ebb2..47e152a 100644 --- a/src/shader.c +++ b/src/shader.c @@ -1,6 +1,6 @@ #include "../include/shader.h" +#include "../include/log.h" #include "../include/egl.h" -#include <stdio.h> #include <assert.h> static bool print_compile_errors = false; @@ -12,7 +12,7 @@ static int min_int(int a, int b) { static unsigned int load_shader(gsr_egl *egl, unsigned int type, const char *source) { unsigned int shader_id = egl->glCreateShader(type); if(shader_id == 0) { - fprintf(stderr, "gsr error: load_shader: failed to create shader, error: %d\n", egl->glGetError()); + gsr_log(GSR_LOG_LEVEL_ERROR, "load_shader: failed to create shader, error: %d", egl->glGetError()); return 0; } @@ -28,7 +28,7 @@ static unsigned int load_shader(gsr_egl *egl, unsigned int type, const char *sou if(info_length > 1 && print_compile_errors) { char info_log[4096]; egl->glGetShaderInfoLog(shader_id, min_int(4096, info_length), NULL, info_log); - fprintf(stderr, "gsr error: load_shader: failed to compile shader, error:\n%s\nshader source:\n%s\n", info_log, source); + gsr_log(GSR_LOG_LEVEL_ERROR, "load_shader: failed to compile shader, error:\n%s\nshader source:\n%s", info_log, source); } egl->glDeleteShader(shader_id); @@ -59,7 +59,7 @@ static unsigned int load_program(gsr_egl *egl, const char *vertex_shader, const program_id = egl->glCreateProgram(); if(program_id == 0) { - fprintf(stderr, "gsr error: load_program: failed to create shader program, error: %d\n", egl->glGetError()); + gsr_log(GSR_LOG_LEVEL_ERROR, "load_program: failed to create shader program, error: %d", egl->glGetError()); goto done; } @@ -79,7 +79,7 @@ static unsigned int load_program(gsr_egl *egl, const char *vertex_shader, const if(info_length > 1) { char info_log[4096]; egl->glGetProgramInfoLog(program_id, min_int(4096, info_length), NULL, info_log); - fprintf(stderr, "gsr error: load program: linking shader program failed, error:\n%s\n", info_log); + gsr_log(GSR_LOG_LEVEL_ERROR, "load program: linking shader program failed, error:\n%s", info_log); } goto done; @@ -106,7 +106,7 @@ int gsr_shader_init(gsr_shader *self, gsr_egl *egl, const char *vertex_shader, c self->program_id = 0; if(!vertex_shader && !fragment_shader) { - fprintf(stderr, "gsr error: gsr_shader_init: vertex and fragment shader can't be NULL at the same time\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_shader_init: vertex and fragment shader can't be NULL at the same time"); return -1; } diff --git a/src/sound.cpp b/src/sound.c index 4e04d8f..2d5d4c2 100644 --- a/src/sound.cpp +++ b/src/sound.c @@ -1,14 +1,12 @@ -#include "../include/sound.hpp" -extern "C" { +#include "../include/sound.h" +#include "../include/log.h" #include "../include/utils.h" -} #include <stdlib.h> #include <stdio.h> #include <string.h> -#include <cmath> -#include <time.h> -#include <mutex> +#include <assert.h> +#include <pthread.h> #include <pulse/pulseaudio.h> #include <pulse/mainloop.h> @@ -33,13 +31,13 @@ extern "C" { } \ } while(false); -enum class DeviceType { - STANDARD, - DEFAULT_OUTPUT, - DEFAULT_INPUT -}; +typedef enum { + DEVICE_TYPE_STANDARD, + DEVICE_TYPE_DEFAULT_OUTPUT, + DEVICE_TYPE_DEFAULT_INPUT +} DeviceType; -struct pa_handle { +typedef struct { pa_context *context; pa_stream *stream; pa_mainloop *mainloop; @@ -56,7 +54,8 @@ struct pa_handle { pa_buffer_attr attr; pa_sample_spec ss; - std::mutex reconnect_mutex; + pthread_mutex_t reconnect_mutex; + bool reconnect_mutex_initialized; DeviceType device_type; char stream_name[256]; char node_name[256]; @@ -69,7 +68,7 @@ struct pa_handle { pa_proplist *proplist; bool connected; -}; +} pa_handle; static void pa_sound_device_free(pa_handle *p) { assert(p); @@ -100,17 +99,23 @@ static void pa_sound_device_free(pa_handle *p) { p->output_data = NULL; } + if (p->reconnect_mutex_initialized) { + pthread_mutex_destroy(&p->reconnect_mutex); + p->reconnect_mutex_initialized = false; + } + pa_xfree(p); } -static void subscribe_update_default_devices(pa_context*, const pa_server_info *server_info, void *userdata) { +static void subscribe_update_default_devices(pa_context *ctx, const pa_server_info *server_info, void *userdata) { + (void)ctx; pa_handle *handle = (pa_handle*)userdata; - std::lock_guard<std::mutex> lock(handle->reconnect_mutex); + pthread_mutex_lock(&handle->reconnect_mutex); if(server_info->default_sink_name) { // TODO: Size check snprintf(handle->default_output_device_name, sizeof(handle->default_output_device_name), "%s.monitor", server_info->default_sink_name); - if(handle->device_type == DeviceType::DEFAULT_OUTPUT && strcmp(handle->device_name, handle->default_output_device_name) != 0) { + if(handle->device_type == DEVICE_TYPE_DEFAULT_OUTPUT && strcmp(handle->device_name, handle->default_output_device_name) != 0) { handle->reconnect = true; handle->reconnect_last_tried_seconds = clock_get_monotonic_seconds(); // TODO: Size check @@ -121,13 +126,15 @@ static void subscribe_update_default_devices(pa_context*, const pa_server_info * if(server_info->default_source_name) { // TODO: Size check snprintf(handle->default_input_device_name, sizeof(handle->default_input_device_name), "%s", server_info->default_source_name); - if(handle->device_type == DeviceType::DEFAULT_INPUT && strcmp(handle->device_name, handle->default_input_device_name) != 0) { + if(handle->device_type == DEVICE_TYPE_DEFAULT_INPUT && strcmp(handle->device_name, handle->default_input_device_name) != 0) { handle->reconnect = true; handle->reconnect_last_tried_seconds = clock_get_monotonic_seconds(); // TODO: Size check snprintf(handle->device_name, sizeof(handle->device_name), "%s", handle->default_input_device_name); } } + + pthread_mutex_unlock(&handle->reconnect_mutex); } static void subscribe_cb(pa_context *c, pa_subscription_event_type_t t, uint32_t idx, void *userdata) { @@ -140,7 +147,8 @@ static void subscribe_cb(pa_context *c, pa_subscription_event_type_t t, uint32_t } } -static void store_default_devices(pa_context*, const pa_server_info *server_info, void *userdata) { +static void store_default_devices(pa_context *ctx, const pa_server_info *server_info, void *userdata) { + (void)ctx; pa_handle *handle = (pa_handle*)userdata; if(server_info->default_sink_name) snprintf(handle->default_output_device_name, sizeof(handle->default_output_device_name), "%s.monitor", server_info->default_sink_name); @@ -151,7 +159,7 @@ static void store_default_devices(pa_context*, const pa_server_info *server_info static bool startup_get_default_devices(pa_handle *p, const char *device_name) { pa_operation *pa = pa_context_get_server_info(p->context, store_default_devices, p); while(pa) { - pa_operation_state state = pa_operation_get_state(pa); + pa_operation_state_t state = pa_operation_get_state(pa); if(state == PA_OPERATION_DONE) { pa_operation_unref(pa); break; @@ -163,19 +171,19 @@ static bool startup_get_default_devices(pa_handle *p, const char *device_name) { } if(p->default_output_device_name[0] == '\0') { - fprintf(stderr, "gsr error: failed to find default audio output device\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "failed to find default audio output device"); return false; } if(strcmp(device_name, "default_output") == 0) { snprintf(p->device_name, sizeof(p->device_name), "%s", p->default_output_device_name); - p->device_type = DeviceType::DEFAULT_OUTPUT; + p->device_type = DEVICE_TYPE_DEFAULT_OUTPUT; } else if(strcmp(device_name, "default_input") == 0) { snprintf(p->device_name, sizeof(p->device_name), "%s", p->default_input_device_name); - p->device_type = DeviceType::DEFAULT_INPUT; + p->device_type = DEVICE_TYPE_DEFAULT_INPUT; } else { snprintf(p->device_name, sizeof(p->device_name), "%s", device_name); - p->device_type = DeviceType::STANDARD; + p->device_type = DEVICE_TYPE_STANDARD; } return true; @@ -198,17 +206,26 @@ static pa_handle* pa_sound_device_new(const char *server, snprintf(p->node_name, sizeof(p->node_name), "%s", name); snprintf(p->stream_name, sizeof(p->stream_name), "%s", stream_name); + if(pthread_mutex_init(&p->reconnect_mutex, NULL) != 0) { + gsr_log(GSR_LOG_LEVEL_ERROR, "failed to initialize reconnect mutex"); + *rerror = -1; + pa_xfree(p); + return NULL; + } + p->reconnect_mutex_initialized = true; + p->reconnect = true; p->reconnect_last_tried_seconds = clock_get_monotonic_seconds() - (RECONNECT_TRY_TIMEOUT_SECONDS * 1000.0 * 2.0); p->default_output_device_name[0] = '\0'; p->default_input_device_name[0] = '\0'; - p->device_type = DeviceType::STANDARD; + p->device_type = DEVICE_TYPE_STANDARD; const int buffer_size = attr->fragsize; void *buffer = malloc(buffer_size); if(!buffer) { - fprintf(stderr, "gsr error: failed to allocate buffer for audio\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "failed to allocate buffer for audio"); *rerror = -1; + pa_sound_device_free(p); return NULL; } @@ -286,12 +303,12 @@ static bool pa_sound_device_handle_context_recreate(pa_handle *p) { } if (!(p->context = pa_context_new_with_proplist(pa_mainloop_get_api(p->mainloop), p->node_name, p->proplist))) { - fprintf(stderr, "gsr error: pa_context_new_with_proplist failed\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "pa_context_new_with_proplist failed"); goto fail; } - if(pa_context_connect(p->context, nullptr, PA_CONTEXT_NOFLAGS, NULL) < 0) { - fprintf(stderr, "gsr error: pa_context_connect failed\n"); + if(pa_context_connect(p->context, NULL, PA_CONTEXT_NOFLAGS, NULL) < 0) { + gsr_log(GSR_LOG_LEVEL_ERROR, "pa_context_connect failed"); goto fail; } @@ -309,7 +326,8 @@ static bool pa_sound_device_handle_context_recreate(pa_handle *p) { } static bool pa_sound_device_should_reconnect(pa_handle *p, double now, char *device_name, size_t device_name_size) { - std::lock_guard<std::mutex> lock(p->reconnect_mutex); + bool should_reconnect = false; + pthread_mutex_lock(&p->reconnect_mutex); if(!p->reconnect && (!p->stream || !PA_STREAM_IS_GOOD(pa_stream_get_state(p->stream)))) { p->reconnect = true; @@ -320,10 +338,11 @@ static bool pa_sound_device_should_reconnect(pa_handle *p, double now, char *dev p->reconnect_last_tried_seconds = now; // TODO: Size check snprintf(device_name, device_name_size, "%s", p->device_name); - return true; + should_reconnect = true; } - return false; + pthread_mutex_unlock(&p->reconnect_mutex); + return should_reconnect; } static bool pa_sound_device_handle_reconnect(pa_handle *p, char *device_name, size_t device_name_size, double now) { @@ -362,8 +381,9 @@ static bool pa_sound_device_handle_reconnect(pa_handle *p, char *device_name, si pa_mainloop_iterate(p->mainloop, 0, NULL); - std::lock_guard<std::mutex> lock(p->reconnect_mutex); + pthread_mutex_lock(&p->reconnect_mutex); p->reconnect = false; + pthread_mutex_unlock(&p->reconnect_mutex); return true; } @@ -452,7 +472,7 @@ static int pa_sound_device_read(pa_handle *p, double timeout_seconds) { p->read_data = NULL; p->read_length = 0; p->read_index = 0; - + if(pa_stream_drop(p->stream) != 0) goto fail; @@ -469,27 +489,27 @@ static int pa_sound_device_read(pa_handle *p, double timeout_seconds) { return success ? 0 : -1; } -static pa_sample_format_t audio_format_to_pulse_audio_format(AudioFormat audio_format) { +static pa_sample_format_t audio_format_to_pulse_audio_format(gsr_audio_format audio_format) { switch(audio_format) { - case S16: return PA_SAMPLE_S16LE; - case S32: return PA_SAMPLE_S32LE; - case F32: return PA_SAMPLE_FLOAT32LE; + case GSR_AUDIO_FORMAT_S16: return PA_SAMPLE_S16LE; + case GSR_AUDIO_FORMAT_S32: return PA_SAMPLE_S32LE; + case GSR_AUDIO_FORMAT_F32: return PA_SAMPLE_FLOAT32LE; } assert(false); return PA_SAMPLE_S16LE; } -static int audio_format_to_get_bytes_per_sample(AudioFormat audio_format) { +static int audio_format_to_get_bytes_per_sample(gsr_audio_format audio_format) { switch(audio_format) { - case S16: return 2; - case S32: return 4; - case F32: return 4; + case GSR_AUDIO_FORMAT_S16: return 2; + case GSR_AUDIO_FORMAT_S32: return 4; + case GSR_AUDIO_FORMAT_F32: return 4; } assert(false); return 2; } -int sound_device_get_by_name(SoundDevice *device, const char *node_name, const char *device_name, const char *description, unsigned int num_channels, unsigned int period_frame_size, AudioFormat audio_format) { +int sound_device_get_by_name(SoundDevice *device, const char *node_name, const char *device_name, const char *description, unsigned int num_channels, unsigned int period_frame_size, gsr_audio_format audio_format) { pa_sample_spec ss; ss.format = audio_format_to_pulse_audio_format(audio_format); ss.rate = 48000; @@ -503,9 +523,9 @@ int sound_device_get_by_name(SoundDevice *device, const char *node_name, const c buffer_attr.maxlength = buffer_attr.fragsize; int error = 0; - pa_handle *handle = pa_sound_device_new(nullptr, node_name, device_name, description, &ss, &buffer_attr, &error); + pa_handle *handle = pa_sound_device_new(NULL, node_name, device_name, description, &ss, &buffer_attr, &error); if(!handle) { - fprintf(stderr, "gsr error: pa_sound_device_new() failed: %s. Audio input device %s might not be valid\n", pa_strerror(error), device_name); + gsr_log(GSR_LOG_LEVEL_ERROR, "pa_sound_device_new() failed: %s. Audio input device %s might not be valid", pa_strerror(error), device_name); return -1; } @@ -543,7 +563,7 @@ void sound_device_flush(SoundDevice *device) { int sound_device_read_next_chunk(SoundDevice *device, void **buffer, double timeout_sec, double *latency_seconds) { pa_handle *pa = (pa_handle*)device->handle; if(pa_sound_device_read(pa, timeout_sec) < 0) { - //fprintf(stderr, "pa_simple_read() failed: %s\n", pa_strerror(error)); + //gsr_log(GSR_LOG_LEVEL_ERROR, "pa_simple_read() failed: %s", pa_strerror(error)); *latency_seconds = 0.0; return -1; } @@ -553,7 +573,7 @@ int sound_device_read_next_chunk(SoundDevice *device, void **buffer, double time } static void pa_state_cb(pa_context *c, void *userdata) { - pa_context_state state = pa_context_get_state(c); + pa_context_state_t state = pa_context_get_state(c); int *pa_ready = (int*)userdata; switch(state) { case PA_CONTEXT_UNCONNECTED: @@ -572,29 +592,38 @@ static void pa_state_cb(pa_context *c, void *userdata) { } } -static void pa_sourcelist_cb(pa_context*, const pa_source_info *source_info, int eol, void *userdata) { +static void pa_sourcelist_cb(pa_context *ctx, const pa_source_info *source_info, int eol, void *userdata) { + (void)ctx; if(eol > 0) return; - AudioDevices *audio_devices = (AudioDevices*)userdata; - audio_devices->audio_inputs.push_back({ source_info->name, source_info->description }); + gsr_audio_devices *audio_devices = (gsr_audio_devices*)userdata; + if(!gsr_array_ensure_capacity((void**)&audio_devices->items, audio_devices->num_items, &audio_devices->capacity_items, sizeof(gsr_audio_device))) + return; + + gsr_audio_device *audio_device = &audio_devices->items[audio_devices->num_items]; + snprintf(audio_device->name, sizeof(audio_device->name), "%s", source_info->name); + snprintf(audio_device->description, sizeof(audio_device->description), "%s", source_info->description); + ++audio_devices->num_items; } -static void pa_server_info_cb(pa_context*, const pa_server_info *server_info, void *userdata) { - AudioDevices *audio_devices = (AudioDevices*)userdata; +static void pa_server_info_cb(pa_context *ctx, const pa_server_info *server_info, void *userdata) { + (void)ctx; + gsr_audio_devices *audio_devices = (gsr_audio_devices*)userdata; if(server_info->default_sink_name) - audio_devices->default_output = std::string(server_info->default_sink_name) + ".monitor"; + snprintf(audio_devices->default_output, sizeof(audio_devices->default_output), "%s.monitor", server_info->default_sink_name); if(server_info->default_source_name) - audio_devices->default_input = server_info->default_source_name; + snprintf(audio_devices->default_input, sizeof(audio_devices->default_input), "%s", server_info->default_source_name); } -static void server_info_callback(pa_context*, const pa_server_info *server_info, void *userdata) { +static void server_info_callback(pa_context *ctx, const pa_server_info *server_info, void *userdata) { + (void)ctx; bool *is_server_pipewire = (bool*)userdata; if(server_info->server_name && strstr(server_info->server_name, "PipeWire")) *is_server_pipewire = true; } -static void get_pulseaudio_default_inputs(AudioDevices &audio_devices) { +static void get_pulseaudio_default_inputs(gsr_audio_devices *audio_devices) { int state = 0; int pa_ready = 0; pa_operation *pa_op = NULL; @@ -618,7 +647,7 @@ static void get_pulseaudio_default_inputs(AudioDevices &audio_devices) { switch(state) { case 0: { - pa_op = pa_context_get_server_info(ctx, pa_server_info_cb, &audio_devices); + pa_op = pa_context_get_server_info(ctx, pa_server_info_cb, audio_devices); ++state; break; } @@ -639,8 +668,9 @@ static void get_pulseaudio_default_inputs(AudioDevices &audio_devices) { pa_mainloop_free(main_loop); } -AudioDevices get_pulseaudio_inputs() { - AudioDevices audio_devices; +void get_pulseaudio_inputs(gsr_audio_devices *audio_devices) { + memset(audio_devices, 0, sizeof(*audio_devices)); + int state = 0; int pa_ready = 0; pa_operation *pa_op = NULL; @@ -650,7 +680,7 @@ AudioDevices get_pulseaudio_inputs() { pa_mainloop *main_loop = pa_mainloop_new(); if(!main_loop) - return audio_devices; + return; pa_context *ctx = pa_context_new(pa_mainloop_get_api(main_loop), "gpu-screen-recorder"); if(pa_context_connect(ctx, NULL, PA_CONTEXT_NOFLAGS, NULL) < 0) @@ -667,7 +697,7 @@ AudioDevices get_pulseaudio_inputs() { switch(state) { case 0: { - pa_op = pa_context_get_source_info_list(ctx, pa_sourcelist_cb, &audio_devices); + pa_op = pa_context_get_source_info_list(ctx, pa_sourcelist_cb, audio_devices); ++state; break; } @@ -686,10 +716,20 @@ AudioDevices get_pulseaudio_inputs() { pa_context_disconnect(ctx); pa_context_unref(ctx); pa_mainloop_free(main_loop); - return audio_devices; } -bool pulseaudio_server_is_pipewire() { +void gsr_audio_devices_deinit(gsr_audio_devices *self) { + if(self->items) { + free(self->items); + self->items = NULL; + } + self->num_items = 0; + self->capacity_items = 0; + self->default_output[0] = '\0'; + self->default_input[0] = '\0'; +} + +bool pulseaudio_server_is_pipewire(void) { int state = 0; int pa_ready = 0; pa_operation *pa_op = NULL; diff --git a/src/utils.c b/src/utils.c index 31853e2..71ad02d 100644 --- a/src/utils.c +++ b/src/utils.c @@ -1,4 +1,5 @@ #include "../include/utils.h" +#include "../include/log.h" #include "../include/window/window.h" #include "../include/capture/capture.h" @@ -33,7 +34,7 @@ double clock_get_monotonic_seconds(void) { bool generate_random_characters(char *buffer, int buffer_size, const char *alphabet, size_t alphabet_size) { /* TODO: Use other functions on other platforms than linux */ if(getrandom(buffer, buffer_size, 0) < buffer_size) { - fprintf(stderr, "Failed to get random bytes, error: %s\n", strerror(errno)); + gsr_log(GSR_LOG_LEVEL_ERROR, "failed to get random bytes, error: %s", strerror(errno)); return false; } @@ -197,7 +198,7 @@ static bool connector_get_property_by_name(int drmfd, drmModeConnectorPtr props, static void for_each_active_monitor_output_drm(const char *card_path, active_monitor_callback callback, void *userdata) { int fd = open(card_path, O_RDONLY); if(fd == -1) { - fprintf(stderr, "gsr error: for_each_active_monitor_output_drm failed, failed to open \"%s\", error: %s\n", card_path, strerror(errno)); + gsr_log(GSR_LOG_LEVEL_ERROR, "for_each_active_monitor_output_drm failed, failed to open \"%s\", error: %s", card_path, strerror(errno)); return; } @@ -365,14 +366,14 @@ bool gl_get_gpu_info(gsr_egl *egl, gsr_gpu_info *info) { info->is_steam_deck = false; if(!gl_vendor) { - fprintf(stderr, "gsr error: failed to get gpu vendor\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "failed to get gpu vendor"); return false; } if(gl_renderer) { for(int i = 0; software_renderers[i]; ++i) { if(strstr(gl_renderer, software_renderers[i])) { - fprintf(stderr, "gsr error: your opengl environment is not properly setup. It's using %s (software rendering) for opengl instead of your graphics card. Please make sure your graphics driver is properly installed\n", software_renderers[i]); + gsr_log(GSR_LOG_LEVEL_ERROR, "your opengl environment is not properly setup. It's using %s (software rendering) for opengl instead of your graphics card. Please make sure your graphics driver is properly installed", software_renderers[i]); return false; } } @@ -388,8 +389,10 @@ bool gl_get_gpu_info(gsr_egl *egl, gsr_gpu_info *info) { info->vendor = GSR_GPU_VENDOR_NVIDIA; else if(strstr(gl_vendor, "Broadcom")) info->vendor = GSR_GPU_VENDOR_BROADCOM; + else if(strstr(gl_vendor, "Mesa") && gl_renderer && strstr(gl_renderer, "Apple")) + info->vendor = GSR_GPU_VENDOR_APPLE; else { - fprintf(stderr, "gsr error: unknown gpu vendor: %s\n", gl_vendor); + gsr_log(GSR_LOG_LEVEL_ERROR, "unknown gpu vendor: %s", gl_vendor); return false; } @@ -449,7 +452,8 @@ bool try_card_has_valid_plane(const char *card_path) { bool gsr_get_valid_card_path(gsr_egl *egl, char *output, bool is_monitor_capture) { if(egl->dri_card_path) { snprintf(output, 128, "%s", egl->dri_card_path); - return is_monitor_capture ? try_card_has_valid_plane(output) : true; + if(!is_monitor_capture || try_card_has_valid_plane(output)) + return true; } for(int i = 0; i < 10; ++i) { @@ -656,7 +660,7 @@ bool get_nvidia_driver_version(int *major, int *minor) { FILE *f = fopen("/proc/driver/nvidia/version", "rb"); if(!f) { - fprintf(stderr, "gsr warning: failed to get nvidia driver version (failed to read /proc/driver/nvidia/version)\n"); + gsr_log(GSR_LOG_LEVEL_WARNING, "failed to get nvidia driver version (failed to read /proc/driver/nvidia/version)"); return false; } @@ -677,8 +681,94 @@ bool get_nvidia_driver_version(int *major, int *minor) { } if(!success) - fprintf(stderr, "gsr warning: failed to get nvidia driver version\n"); + gsr_log(GSR_LOG_LEVEL_WARNING, "failed to get nvidia driver version"); fclose(f); return success; } + +void gsr_string_split(const char *str, char delimiter, gsr_string_split_callback callback, void *userdata) { + const size_t str_len = strlen(str); + size_t index = 0; + while(index < str_len) { + const char *end = strchr(str + index, delimiter); + const size_t end_index = end ? (size_t)(end - str) : str_len; + + if(!callback(str + index, end_index - index, userdata)) + break; + + index = end_index + 1; + } +} + +bool gsr_string_starts_with(const char *str, size_t str_size, const char *substr) { + const size_t substr_len = strlen(substr); + return str_size >= substr_len && memcmp(str, substr, substr_len) == 0; +} + +bool gsr_string_ends_with(const char *str, const char *substr) { + const size_t str_len = strlen(str); + const size_t substr_len = strlen(substr); + return str_len >= substr_len && memcmp(str + str_len - substr_len, substr, substr_len) == 0; +} + +static bool string_to_long(const char *str, size_t size, long *number) { + char number_str[32]; + snprintf(number_str, sizeof(number_str), "%.*s", (int)size, str); + + errno = 0; + *number = strtol(number_str, NULL, 0); + return errno == 0; +} + +bool gsr_string_to_int(const char *str, size_t size, int *number) { + long value = 0; + if(!string_to_long(str, size, &value)) + return false; + *number = value; + return true; +} + +bool gsr_string_to_int64(const char *str, size_t size, int64_t *number) { + long value = 0; + if(!string_to_long(str, size, &value)) + return false; + *number = value; + return true; +} + +bool gsr_array_ensure_capacity(void **array, size_t num_items, size_t *capacity_items, size_t item_size) { + if(num_items + 1 >= *capacity_items) { + size_t new_capacity_items = *capacity_items * 2; + if(new_capacity_items == 0) + new_capacity_items = 32; + + void *new_data = realloc(*array, new_capacity_items * item_size); + if(!new_data) { + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_array_ensure_capacity: failed to reallocate memory"); + return false; + } + + *array = new_data; + *capacity_items = new_capacity_items; + } + return true; +} + +void gsr_get_date_str(char *str, size_t size) { + time_t now = time(NULL); + struct tm *t = localtime(&now); + strftime(str, size - 1, "%Y-%m-%d_%H-%M-%S", t); +} + +void gsr_get_date_only_str(char *str, size_t size) { + time_t now = time(NULL); + struct tm *t = localtime(&now); + strftime(str, size - 1, "%Y-%m-%d", t); +} + +void gsr_get_time_only_str(char *str, size_t size) { + time_t now = time(NULL); + struct tm *t = localtime(&now); + strftime(str, size - 1, "%H-%M-%S", t); +} diff --git a/src/wayland_host_bridge.c b/src/wayland_host_bridge.c index 4e8319b..f9299a4 100644 --- a/src/wayland_host_bridge.c +++ b/src/wayland_host_bridge.c @@ -1,4 +1,5 @@ #include "../include/wayland_host_bridge.h" +#include "../include/log.h" #include <stdio.h> #include <stdlib.h> @@ -84,14 +85,14 @@ static struct wl_display* connect_via_bridge() { waitpid(pid, &status, 0); if(wayland_fd < 0) { - fprintf(stderr, "WaylandHostBridge: gsr-wayland-bridge did not return a wayland fd\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "WaylandHostBridge: gsr-wayland-bridge did not return a wayland fd"); return NULL; } struct wl_display *dpy = wl_display_connect_to_fd(wayland_fd); if(!dpy) { close(wayland_fd); - fprintf(stderr, "WaylandHostBridge: wl_display_connect_to_fd failed\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "WaylandHostBridge: wl_display_connect_to_fd failed"); return NULL; } return dpy; @@ -103,7 +104,7 @@ struct wl_display* wayland_connect_to_host() { struct wl_display *dpy = connect_via_bridge(); if(dpy) return dpy; - fprintf(stderr, "WaylandHostBridge: falling back to sandboxed wl_display_connect\n"); + gsr_log(GSR_LOG_LEVEL_WARNING, "WaylandHostBridge: falling back to sandboxed wl_display_connect"); } return wl_display_connect(NULL); } diff --git a/src/window/wayland.c b/src/window/wayland.c index 6c263c5..75e36f0 100644 --- a/src/window/wayland.c +++ b/src/window/wayland.c @@ -1,4 +1,5 @@ #include "../../include/window/wayland.h" +#include "../../include/log.h" #include "../../include/wayland_host_bridge.h" #include "../../include/vec2.h" @@ -234,12 +235,12 @@ static void registry_add_object(void *data, struct wl_registry *registry, uint32 window_wayland->compositor = wl_registry_bind(registry, name, &wl_compositor_interface, 1); } else if(strcmp(interface, wl_output_interface.name) == 0) { if(version < 4) { - fprintf(stderr, "gsr warning: wl output interface version is < 4, expected >= 4 to capture a monitor\n"); + gsr_log(GSR_LOG_LEVEL_WARNING, "wl output interface version is < 4, expected >= 4 to capture a monitor"); return; } if(window_wayland->num_outputs == GSR_MAX_OUTPUTS) { - fprintf(stderr, "gsr warning: reached maximum outputs (%d), ignoring output %u\n", GSR_MAX_OUTPUTS, name); + gsr_log(GSR_LOG_LEVEL_WARNING, "reached maximum outputs (%d), ignoring output %u", GSR_MAX_OUTPUTS, name); return; } @@ -257,7 +258,7 @@ static void registry_add_object(void *data, struct wl_registry *registry, uint32 wl_output_add_listener(gsr_output->output, &output_listener, gsr_output); } else if(strcmp(interface, zxdg_output_manager_v1_interface.name) == 0) { if(version < 1) { - fprintf(stderr, "gsr warning: xdg output interface version is < 1, expected >= 1 to capture a monitor\n"); + gsr_log(GSR_LOG_LEVEL_WARNING, "xdg output interface version is < 1, expected >= 1 to capture a monitor"); return; } @@ -326,7 +327,7 @@ static const struct zxdg_output_v1_listener xdg_output_listener = { static void gsr_window_wayland_set_monitor_outputs_from_xdg_output(gsr_window_wayland *self) { if(!self->xdg_output_manager) { - fprintf(stderr, "gsr warning: zxdg_output_manager not found. registered monitor positions might be incorrect\n"); + gsr_log(GSR_LOG_LEVEL_WARNING, "zxdg_output_manager not found. registered monitor positions might be incorrect"); return; } @@ -464,7 +465,7 @@ static void gsr_window_wayland_deinit(gsr_window_wayland *self) { static bool gsr_window_wayland_init(gsr_window_wayland *self) { self->display = wayland_connect_to_host(); if(!self->display) { - fprintf(stderr, "gsr error: gsr_window_wayland_init failed: failed to connect to the Wayland server\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_window_wayland_init failed: failed to connect to the Wayland server"); goto fail; } @@ -482,19 +483,19 @@ static bool gsr_window_wayland_init(gsr_window_wayland *self) { gsr_window_wayland_set_monitor_real_positions(self); if(!self->compositor) { - fprintf(stderr, "gsr error: gsr_window_wayland_init failed: failed to find compositor\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_window_wayland_init failed: failed to find compositor"); goto fail; } self->surface = wl_compositor_create_surface(self->compositor); if(!self->surface) { - fprintf(stderr, "gsr error: gsr_window_wayland_init failed: failed to create surface\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_window_wayland_init failed: failed to create surface"); goto fail; } self->window = wl_egl_window_create(self->surface, 16, 16); if(!self->window) { - fprintf(stderr, "gsr error: gsr_window_wayland_init failed: failed to create window\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_window_wayland_init failed: failed to create window"); goto fail; } diff --git a/src/window/window.c b/src/window/window.c index 5ede9fd..460ff7f 100644 --- a/src/window/window.c +++ b/src/window/window.c @@ -1,7 +1,10 @@ #include "../../include/window/window.h" #include <stddef.h> -void gsr_window_destroy(gsr_window *self); +void gsr_window_destroy(gsr_window *self) { + if(self) + self->destroy(self); +} bool gsr_window_process_event(gsr_window *self) { return self->process_event(self); diff --git a/src/window/x11.c b/src/window/x11.c index 3b3c955..2ae62c8 100644 --- a/src/window/x11.c +++ b/src/window/x11.c @@ -1,10 +1,10 @@ #include "../../include/window/x11.h" +#include "../../include/log.h" #include "../../include/vec2.h" #include "../../include/defs.h" #include "../../include/utils.h" -#include <stdio.h> #include <stdlib.h> #include <string.h> #include <stdint.h> @@ -32,7 +32,7 @@ typedef struct { static void store_x11_monitor(const gsr_monitor *monitor, void *userdata) { gsr_window_x11 *window_x11 = userdata; if(window_x11->num_outputs == GSR_MAX_OUTPUTS) { - fprintf(stderr, "gsr warning: reached maximum outputs (%d), ignoring output %s\n", GSR_MAX_OUTPUTS, monitor->name); + gsr_log(GSR_LOG_LEVEL_WARNING, "reached maximum outputs (%d), ignoring output %s", GSR_MAX_OUTPUTS, monitor->name); return; } @@ -68,7 +68,7 @@ static void gsr_window_x11_deinit(gsr_window_x11 *self) { static bool gsr_window_x11_init(gsr_window_x11 *self) { self->window = XCreateWindow(self->display, DefaultRootWindow(self->display), 0, 0, 16, 16, 0, CopyFromParent, InputOutput, CopyFromParent, 0, NULL); if(!self->window) { - fprintf(stderr, "gsr error: gsr_window_x11_init failed: failed to create gl window\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_window_x11_init failed: failed to create gl window"); return false; } diff --git a/subprojects/ffmpeg.wrap b/subprojects/ffmpeg.wrap new file mode 100644 index 0000000..d086138 --- /dev/null +++ b/subprojects/ffmpeg.wrap @@ -0,0 +1,7 @@ +[wrap-file] +directory = ffmpeg-9.0 +source_url = https://ffmpeg.org/releases/ffmpeg-9.0.tar.xz +source_filename = ffmpeg-9.0.tar.xz +source_hash = 7f607a00dd0d28a729d5a4811205812eef01cf6ef6155025febb6f36a9062d52 +patch_directory = ffmpeg +diff_files = ffmpeg-nvenc-runtime-api-version.patch, ffmpeg-mbedtls-default-ca-certs.patch diff --git a/subprojects/mbedtls.wrap b/subprojects/mbedtls.wrap new file mode 100644 index 0000000..dfd3c32 --- /dev/null +++ b/subprojects/mbedtls.wrap @@ -0,0 +1,6 @@ +[wrap-file] +directory = mbedtls-3.6.7 +source_url = https://github.com/Mbed-TLS/mbedtls/releases/download/mbedtls-3.6.7/mbedtls-3.6.7.tar.bz2 +source_filename = mbedtls-3.6.7.tar.bz2 +source_hash = a7e8bcbec0e6f761b4af24f25677626b35f762f68eef79c08677a363212d11f6 +patch_directory = mbedtls diff --git a/subprojects/nv-codec-headers.wrap b/subprojects/nv-codec-headers.wrap new file mode 100644 index 0000000..e3b36de --- /dev/null +++ b/subprojects/nv-codec-headers.wrap @@ -0,0 +1,6 @@ +[wrap-file] +directory = nv-codec-headers-n13.0.19.0 +source_url = https://github.com/FFmpeg/nv-codec-headers/archive/refs/tags/n13.0.19.0.tar.gz +source_filename = nv-codec-headers-13.0.19.0.tar.gz +source_hash = 86d15d1a7c0ac73a0eafdfc57bebfeba7da8264595bf531cf4d8db1c22940116 +patch_directory = nv-codec-headers diff --git a/subprojects/opus.wrap b/subprojects/opus.wrap new file mode 100644 index 0000000..af08d8b --- /dev/null +++ b/subprojects/opus.wrap @@ -0,0 +1,6 @@ +[wrap-file] +directory = opus-1.6.1 +source_url = https://downloads.xiph.org/releases/opus/opus-1.6.1.tar.gz +source_filename = opus-1.6.1.tar.gz +source_hash = 6ffcb593207be92584df15b32466ed64bbec99109f007c82205f0194572411a1 +patch_directory = opus diff --git a/subprojects/packagefiles/ffmpeg-mbedtls-default-ca-certs.patch b/subprojects/packagefiles/ffmpeg-mbedtls-default-ca-certs.patch new file mode 100644 index 0000000..23e48f1 --- /dev/null +++ b/subprojects/packagefiles/ffmpeg-mbedtls-default-ca-certs.patch @@ -0,0 +1,78 @@ +--- a/libavformat/tls_mbedtls.c ++++ b/libavformat/tls_mbedtls.c +@@ -43,6 +43,65 @@ + #include "libavutil/parseutils.h" + #include "libavutil/avstring.h" + #include "libavutil/random_seed.h" ++#include "libavutil/getenv_utf8.h" ++ ++/* ++ * mbedtls has no built-in default certificate location, unlike openssl which ++ * falls back to the location it was compiled with. Without this the peer ++ * certificate can only be verified when the caller passes a ca_file, so look ++ * for the certificate store of the system instead, honoring the same ++ * environment variables as openssl does. ++ */ ++static const char * const default_ca_files[] = { ++ "/etc/ssl/certs/ca-certificates.crt", // debian, ubuntu, arch, alpine, gentoo ++ "/etc/ssl/ca-bundle.pem", // opensuse ++ "/etc/ssl/cert.pem", // openbsd, freebsd, macos ++ "/usr/local/etc/ssl/cert.pem", // freebsd ports ++}; ++ ++static const char * const default_ca_dirs[] = { ++ "/etc/ssl/certs", ++ "/etc/pki/tls/certs", ++}; ++ ++/* A positive return value from mbedtls means that only some of the certificates failed to parse, which is not fatal */ ++static int mbedtls_load_default_ca_certs(URLContext *h, mbedtls_x509_crt *ca_cert) ++{ ++ char *env_ca_file = getenv_utf8("SSL_CERT_FILE"); ++ char *env_ca_dir = getenv_utf8("SSL_CERT_DIR"); ++ int loaded = 0; ++ ++ if (env_ca_file && mbedtls_x509_crt_parse_file(ca_cert, env_ca_file) >= 0) ++ loaded = 1; ++ ++ if (!loaded && env_ca_dir && mbedtls_x509_crt_parse_path(ca_cert, env_ca_dir) >= 0) ++ loaded = 1; ++ ++ freeenv_utf8(env_ca_file); ++ freeenv_utf8(env_ca_dir); ++ ++ for (size_t i = 0; !loaded && i < FF_ARRAY_ELEMS(default_ca_files); i++) { ++ if (mbedtls_x509_crt_parse_file(ca_cert, default_ca_files[i]) >= 0) { ++ av_log(h, AV_LOG_VERBOSE, "loaded CA certificates from %s\n", default_ca_files[i]); ++ loaded = 1; ++ } ++ } ++ ++ for (size_t i = 0; !loaded && i < FF_ARRAY_ELEMS(default_ca_dirs); i++) { ++ if (mbedtls_x509_crt_parse_path(ca_cert, default_ca_dirs[i]) >= 0) { ++ av_log(h, AV_LOG_VERBOSE, "loaded CA certificates from %s\n", default_ca_dirs[i]); ++ loaded = 1; ++ } ++ } ++ ++ if (!loaded) { ++ av_log(h, AV_LOG_WARNING, "unable to find the CA certificates of the system, " ++ "certificate verification is going to fail\n"); ++ return AVERROR(ENOENT); ++ } ++ ++ return 0; ++} + + static int mbedtls_x509_fingerprint(char *cert_buf, size_t cert_sz, char **fingerprint) + { +@@ -555,6 +614,9 @@ + av_log(h, AV_LOG_ERROR, "mbedtls_x509_crt_parse_file for CA cert returned %d\n", ret); + goto fail; + } ++ } else if (shr->verify) { ++ // Only a warning is logged when this fails, matching what the openssl backend does ++ mbedtls_load_default_ca_certs(h, &tls_ctx->ca_cert); + } + + // load own certificate diff --git a/subprojects/packagefiles/ffmpeg-nvenc-runtime-api-version.patch b/subprojects/packagefiles/ffmpeg-nvenc-runtime-api-version.patch new file mode 100644 index 0000000..eb3bd5f --- /dev/null +++ b/subprojects/packagefiles/ffmpeg-nvenc-runtime-api-version.patch @@ -0,0 +1,831 @@ +--- a/libavcodec/nvenc.c ++++ b/libavcodec/nvenc.c +@@ -235,7 +235,15 @@ + + static void nvenc_print_driver_requirement(AVCodecContext *avctx, int level) + { +-#if NVENCAPI_CHECK_VERSION(13, 2) ++/* With runtime API version negotiation the build-time header version no ++ * longer dictates the required driver; the runtime floor is API 11.1. */ ++#if 1 ++# if defined(_WIN32) || defined(__CYGWIN__) ++ const char *minver = "471.41"; ++# else ++ const char *minver = "470.57.02"; ++# endif ++#elif NVENCAPI_CHECK_VERSION(13, 2) + const char *minver = "(unknown)"; + #elif NVENCAPI_CHECK_VERSION(13, 1) + const char *minver = "610.00"; +@@ -279,6 +287,193 @@ + #define to_nv_color_trc(n) (uint32_t)(n) + #endif + ++/* ++ * Runtime NVENC API version negotiation, see nvenc.h. ++ * ++ * Each row holds the exact NV_ENC_*_VER constants of one SDK's nvEncodeAPI.h. ++ * A struct version word is composed as ++ * major | (minor << 24) | (struct_revision << 16) | (0x7 << 28) ++ * optionally OR'd with (1u << 31); the driver validates the whole word, so ++ * presenting a row of these values is indistinguishable from an FFmpeg that ++ * was actually compiled against that SDK's headers. ++ */ ++#define NVENC_API_WORD(major, minor) \ ++ ((uint32_t)(major) | ((uint32_t)(minor) << 24)) ++#define NVENC_STRUCT_VER(major, minor, rev) \ ++ (NVENC_API_WORD(major, minor) | ((uint32_t)(rev) << 16) | (0x7u << 28)) ++#define NVENC_STRUCT_VER_RES(major, minor, rev) \ ++ (NVENC_STRUCT_VER(major, minor, rev) | (1u << 31)) ++ ++// Newest first; negotiation picks the first row the driver supports. ++static const NvencApiVersion nvenc_api_versions[] = { ++ { // SDK 13.0, driver >= 570.0 (Linux) / 570.0 (Windows) ++ .packed = NVENC_API_PACKED(13, 0), ++ .api = NVENC_API_WORD(13, 0), ++ .function_list = NVENC_STRUCT_VER(13, 0, 2), ++ .open_session_params = NVENC_STRUCT_VER(13, 0, 1), ++ .caps_param = NVENC_STRUCT_VER(13, 0, 1), ++ .initialize_params = NVENC_STRUCT_VER_RES(13, 0, 7), ++ .config = NVENC_STRUCT_VER_RES(13, 0, 9), ++ .preset_config = NVENC_STRUCT_VER_RES(13, 0, 5), ++ .pic_params = NVENC_STRUCT_VER_RES(13, 0, 7), ++ .lock_bitstream = NVENC_STRUCT_VER_RES(13, 0, 2), ++ .lock_input_buffer = NVENC_STRUCT_VER(13, 0, 1), ++ .map_input_resource = NVENC_STRUCT_VER(13, 0, 4), ++ .register_resource = NVENC_STRUCT_VER(13, 0, 5), ++ .create_input_buffer = NVENC_STRUCT_VER(13, 0, 2), ++ .create_bitstream_buffer = NVENC_STRUCT_VER(13, 0, 1), ++ .reconfigure_params = NVENC_STRUCT_VER_RES(13, 0, 2), ++ .sequence_param_payload = NVENC_STRUCT_VER(13, 0, 1), ++ }, ++ { // SDK 12.2, driver >= 550.54.14 (Linux) / 551.76 (Windows) ++ .packed = NVENC_API_PACKED(12, 2), ++ .api = NVENC_API_WORD(12, 2), ++ .function_list = NVENC_STRUCT_VER(12, 2, 2), ++ .open_session_params = NVENC_STRUCT_VER(12, 2, 1), ++ .caps_param = NVENC_STRUCT_VER(12, 2, 1), ++ .initialize_params = NVENC_STRUCT_VER_RES(12, 2, 7), ++ .config = NVENC_STRUCT_VER_RES(12, 2, 9), ++ .preset_config = NVENC_STRUCT_VER_RES(12, 2, 5), ++ .pic_params = NVENC_STRUCT_VER_RES(12, 2, 7), ++ .lock_bitstream = NVENC_STRUCT_VER_RES(12, 2, 2), ++ .lock_input_buffer = NVENC_STRUCT_VER(12, 2, 1), ++ .map_input_resource = NVENC_STRUCT_VER(12, 2, 4), ++ .register_resource = NVENC_STRUCT_VER(12, 2, 5), ++ .create_input_buffer = NVENC_STRUCT_VER(12, 2, 2), ++ .create_bitstream_buffer = NVENC_STRUCT_VER(12, 2, 1), ++ .reconfigure_params = NVENC_STRUCT_VER_RES(12, 2, 2), ++ .sequence_param_payload = NVENC_STRUCT_VER(12, 2, 1), ++ }, ++ { // SDK 12.1, driver >= 530.41.03 (Linux) / 531.61 (Windows) ++ .packed = NVENC_API_PACKED(12, 1), ++ .api = NVENC_API_WORD(12, 1), ++ .function_list = NVENC_STRUCT_VER(12, 1, 2), ++ .open_session_params = NVENC_STRUCT_VER(12, 1, 1), ++ .caps_param = NVENC_STRUCT_VER(12, 1, 1), ++ .initialize_params = NVENC_STRUCT_VER_RES(12, 1, 6), ++ .config = NVENC_STRUCT_VER_RES(12, 1, 8), ++ .preset_config = NVENC_STRUCT_VER_RES(12, 1, 4), ++ .pic_params = NVENC_STRUCT_VER_RES(12, 1, 6), ++ .lock_bitstream = NVENC_STRUCT_VER_RES(12, 1, 1), ++ .lock_input_buffer = NVENC_STRUCT_VER(12, 1, 1), ++ .map_input_resource = NVENC_STRUCT_VER(12, 1, 4), ++ .register_resource = NVENC_STRUCT_VER(12, 1, 4), ++ .create_input_buffer = NVENC_STRUCT_VER(12, 1, 1), ++ .create_bitstream_buffer = NVENC_STRUCT_VER(12, 1, 1), ++ .reconfigure_params = NVENC_STRUCT_VER_RES(12, 1, 1), ++ .sequence_param_payload = NVENC_STRUCT_VER(12, 1, 1), ++ }, ++ { // SDK 12.0, driver >= 520.56.06 (Linux) / 522.25 (Windows) ++ .packed = NVENC_API_PACKED(12, 0), ++ .api = NVENC_API_WORD(12, 0), ++ .function_list = NVENC_STRUCT_VER(12, 0, 2), ++ .open_session_params = NVENC_STRUCT_VER(12, 0, 1), ++ .caps_param = NVENC_STRUCT_VER(12, 0, 1), ++ .initialize_params = NVENC_STRUCT_VER_RES(12, 0, 5), ++ .config = NVENC_STRUCT_VER_RES(12, 0, 8), ++ .preset_config = NVENC_STRUCT_VER_RES(12, 0, 4), ++ .pic_params = NVENC_STRUCT_VER_RES(12, 0, 6), ++ .lock_bitstream = NVENC_STRUCT_VER(12, 0, 2), ++ .lock_input_buffer = NVENC_STRUCT_VER(12, 0, 1), ++ .map_input_resource = NVENC_STRUCT_VER(12, 0, 4), ++ .register_resource = NVENC_STRUCT_VER(12, 0, 4), ++ .create_input_buffer = NVENC_STRUCT_VER(12, 0, 1), ++ .create_bitstream_buffer = NVENC_STRUCT_VER(12, 0, 1), ++ .reconfigure_params = NVENC_STRUCT_VER_RES(12, 0, 1), ++ .sequence_param_payload = NVENC_STRUCT_VER(12, 0, 1), ++ }, ++ { // SDK 11.1, driver >= 470.57.02 (Linux) / 471.41 (Windows) - Kepler's last branch ++ .packed = NVENC_API_PACKED(11, 1), ++ .api = NVENC_API_WORD(11, 1), ++ .function_list = NVENC_STRUCT_VER(11, 1, 2), ++ .open_session_params = NVENC_STRUCT_VER(11, 1, 1), ++ .caps_param = NVENC_STRUCT_VER(11, 1, 1), ++ .initialize_params = NVENC_STRUCT_VER_RES(11, 1, 5), ++ .config = NVENC_STRUCT_VER_RES(11, 1, 7), ++ .preset_config = NVENC_STRUCT_VER_RES(11, 1, 4), ++ .pic_params = NVENC_STRUCT_VER_RES(11, 1, 4), ++ .lock_bitstream = NVENC_STRUCT_VER(11, 1, 1), ++ .lock_input_buffer = NVENC_STRUCT_VER(11, 1, 1), ++ .map_input_resource = NVENC_STRUCT_VER(11, 1, 4), ++ .register_resource = NVENC_STRUCT_VER(11, 1, 3), ++ .create_input_buffer = NVENC_STRUCT_VER(11, 1, 1), ++ .create_bitstream_buffer = NVENC_STRUCT_VER(11, 1, 1), ++ .reconfigure_params = NVENC_STRUCT_VER_RES(11, 1, 1), ++ .sequence_param_payload = NVENC_STRUCT_VER(11, 1, 1), ++ }, ++}; ++ ++/* The newest row must be exactly what this build would present anyway; if any ++ * of these fire, the headers changed and the whole table needs re-validation. */ ++_Static_assert(NVENC_API_WORD(13, 0) == NVENCAPI_VERSION, "nvenc api version table outdated"); ++_Static_assert(NVENC_STRUCT_VER(13, 0, 2) == NV_ENCODE_API_FUNCTION_LIST_VER, "nvenc api version table outdated"); ++_Static_assert(NVENC_STRUCT_VER(13, 0, 1) == NV_ENC_OPEN_ENCODE_SESSION_EX_PARAMS_VER, "nvenc api version table outdated"); ++_Static_assert(NVENC_STRUCT_VER(13, 0, 1) == NV_ENC_CAPS_PARAM_VER, "nvenc api version table outdated"); ++_Static_assert(NVENC_STRUCT_VER_RES(13, 0, 7) == NV_ENC_INITIALIZE_PARAMS_VER, "nvenc api version table outdated"); ++_Static_assert(NVENC_STRUCT_VER_RES(13, 0, 9) == NV_ENC_CONFIG_VER, "nvenc api version table outdated"); ++_Static_assert(NVENC_STRUCT_VER_RES(13, 0, 5) == NV_ENC_PRESET_CONFIG_VER, "nvenc api version table outdated"); ++_Static_assert(NVENC_STRUCT_VER_RES(13, 0, 7) == NV_ENC_PIC_PARAMS_VER, "nvenc api version table outdated"); ++_Static_assert(NVENC_STRUCT_VER_RES(13, 0, 2) == NV_ENC_LOCK_BITSTREAM_VER, "nvenc api version table outdated"); ++_Static_assert(NVENC_STRUCT_VER(13, 0, 1) == NV_ENC_LOCK_INPUT_BUFFER_VER, "nvenc api version table outdated"); ++_Static_assert(NVENC_STRUCT_VER(13, 0, 4) == NV_ENC_MAP_INPUT_RESOURCE_VER, "nvenc api version table outdated"); ++_Static_assert(NVENC_STRUCT_VER(13, 0, 5) == NV_ENC_REGISTER_RESOURCE_VER, "nvenc api version table outdated"); ++_Static_assert(NVENC_STRUCT_VER(13, 0, 2) == NV_ENC_CREATE_INPUT_BUFFER_VER, "nvenc api version table outdated"); ++_Static_assert(NVENC_STRUCT_VER(13, 0, 1) == NV_ENC_CREATE_BITSTREAM_BUFFER_VER, "nvenc api version table outdated"); ++_Static_assert(NVENC_STRUCT_VER_RES(13, 0, 2) == NV_ENC_RECONFIGURE_PARAMS_VER, "nvenc api version table outdated"); ++_Static_assert(NVENC_STRUCT_VER(13, 0, 1) == NV_ENC_SEQUENCE_PARAM_PAYLOAD_VER, "nvenc api version table outdated"); ++ ++/* Layout guards for the compatibility padding after NV_ENC_INITIALIZE_PARAMS / ++ * NV_ENC_RECONFIGURE_PARAMS / NV_ENC_LOCK_BITSTREAM (their pre-12.2 layouts ++ * are up to 8 bytes larger: 1808, 1824 and 1552 bytes respectively) and for ++ * the legacy bit-depth pokes below. */ ++_Static_assert(sizeof(NV_ENC_INITIALIZE_PARAMS) == 1800, "unexpected NV_ENC_INITIALIZE_PARAMS layout"); ++_Static_assert(sizeof(NV_ENC_RECONFIGURE_PARAMS) == 1816, "unexpected NV_ENC_RECONFIGURE_PARAMS layout"); ++_Static_assert(sizeof(NV_ENC_LOCK_BITSTREAM) == 1544, "unexpected NV_ENC_LOCK_BITSTREAM layout"); ++_Static_assert(offsetof(NV_ENC_CONFIG_HEVC, tier) == 4 && offsetof(NV_ENC_CONFIG_HEVC, idrPeriod) == 20, ++ "unexpected NV_ENC_CONFIG_HEVC layout"); ++_Static_assert(offsetof(NV_ENC_CONFIG_AV1, tier) == 4 && offsetof(NV_ENC_CONFIG_AV1, idrPeriod) == 20, ++ "unexpected NV_ENC_CONFIG_AV1 layout"); ++ ++/* ++ * SDK 12.2 removed the NV_ENC_CONFIG_HEVC.pixelBitDepthMinus8 and ++ * NV_ENC_CONFIG_AV1.inputPixelBitDepthMinus8/pixelBitDepthMinus8 bitfields ++ * and replaced them with the inputBitDepth/outputBitDepth enums that live at ++ * different offsets. Drivers negotiated to API < 12.2 still read the old bit ++ * positions (reserved-and-zero in the compiled 12.2+ layout), so 10 bit ++ * encoding on those drivers has to set the old bits by hand. ++ * ++ * Verified against the SDK 11.1/12.0/12.1 headers: both codec configs keep ++ * their flag bitfields in the uint32 word at byte offset 16 (between ++ * maxCUSize/maxPartSize at 12 and idrPeriod at 20, anchored by the asserts ++ * above). Within that word: ++ * NV_ENC_CONFIG_HEVC: pixelBitDepthMinus8 = bits 11-13 ++ * NV_ENC_CONFIG_AV1: inputPixelBitDepthMinus8 = bits 12-14, ++ * pixelBitDepthMinus8 = bits 15-17 ++ */ ++static void nvenc_legacy_bitfield_poke(void *codec_config, size_t word_offset, ++ unsigned bit_offset, unsigned width, uint32_t value) ++{ ++ uint32_t word; ++ memcpy(&word, (uint8_t*)codec_config + word_offset, sizeof(word)); ++ word &= ~(((1u << width) - 1) << bit_offset); ++ word |= (value & ((1u << width) - 1)) << bit_offset; ++ memcpy((uint8_t*)codec_config + word_offset, &word, sizeof(word)); ++} ++ ++static void nvenc_set_legacy_hevc_bit_depth(NV_ENC_CONFIG_HEVC *hevc, uint32_t depth_minus8) ++{ ++ nvenc_legacy_bitfield_poke(hevc, 16, 11, 3, depth_minus8); ++} ++ ++#if CONFIG_AV1_NVENC_ENCODER ++static void nvenc_set_legacy_av1_bit_depth(NV_ENC_CONFIG_AV1 *av1, ++ uint32_t input_depth_minus8, uint32_t output_depth_minus8) ++{ ++ nvenc_legacy_bitfield_poke(av1, 16, 12, 3, input_depth_minus8); ++ nvenc_legacy_bitfield_poke(av1, 16, 15, 3, output_depth_minus8); ++} ++#endif ++ + static av_cold int nvenc_load_libraries(AVCodecContext *avctx) + { + NvencContext *ctx = avctx->priv_data; +@@ -303,16 +498,35 @@ + + av_log(avctx, AV_LOG_VERBOSE, "Loaded Nvenc version %d.%d\n", nvenc_max_ver >> 4, nvenc_max_ver & 0xf); + +- if ((NVENCAPI_MAJOR_VERSION << 4 | NVENCAPI_MINOR_VERSION) > nvenc_max_ver) { ++ /* Negotiate: present the newest API version both the driver and this ++ * build support instead of failing on drivers older than the headers. */ ++ ctx->api_vers = NULL; ++ for (int i = 0; i < (int)FF_ARRAY_ELEMS(nvenc_api_versions); i++) { ++ if (nvenc_api_versions[i].packed <= nvenc_max_ver) { ++ ctx->api_vers = &nvenc_api_versions[i]; ++ break; ++ } ++ } ++ ++ if (!ctx->api_vers) { + av_log(avctx, AV_LOG_ERROR, "Driver does not support the required nvenc API version. " + "Required: %d.%d Found: %d.%d\n", +- NVENCAPI_MAJOR_VERSION, NVENCAPI_MINOR_VERSION, ++ 11, 1, + nvenc_max_ver >> 4, nvenc_max_ver & 0xf); + nvenc_print_driver_requirement(avctx, AV_LOG_ERROR); + return AVERROR(ENOSYS); + } + +- dl_fn->nvenc_funcs.version = NV_ENCODE_API_FUNCTION_LIST_VER; ++ ctx->api_version = ctx->api_vers->packed; ++ ++ if (ctx->api_version < NVENC_API_PACKED(NVENCAPI_MAJOR_VERSION, NVENCAPI_MINOR_VERSION)) ++ av_log(avctx, AV_LOG_VERBOSE, "Driver supports nvenc API up to %d.%d (compiled for %d.%d), " ++ "using API %d.%d; features requiring a newer driver are disabled\n", ++ nvenc_max_ver >> 4, nvenc_max_ver & 0xf, ++ NVENCAPI_MAJOR_VERSION, NVENCAPI_MINOR_VERSION, ++ ctx->api_version >> 4, ctx->api_version & 0xf); ++ ++ dl_fn->nvenc_funcs.version = ctx->api_vers->function_list; + + err = dl_fn->nvenc_dl->NvEncodeAPICreateInstance(&dl_fn->nvenc_funcs); + if (err != NV_ENC_SUCCESS) +@@ -353,8 +567,8 @@ + NV_ENCODE_API_FUNCTION_LIST *p_nvenc = &ctx->nvenc_dload_funcs.nvenc_funcs; + NVENCSTATUS ret; + +- params.version = NV_ENC_OPEN_ENCODE_SESSION_EX_PARAMS_VER; +- params.apiVersion = NVENCAPI_VERSION; ++ params.version = ctx->api_vers->open_session_params; ++ params.apiVersion = ctx->api_vers->api; + if (ctx->d3d11_device) { + params.device = ctx->d3d11_device; + params.deviceType = NV_ENC_DEVICE_TYPE_DIRECTX; +@@ -415,7 +629,7 @@ + NV_ENC_CAPS_PARAM params = { 0 }; + int ret, val = 0; + +- params.version = NV_ENC_CAPS_PARAM_VER; ++ params.version = ctx->api_vers->caps_param; + params.capsToQuery = cap; + + ret = p_nvenc->nvEncGetEncodeCaps(ctx->nvencoder, ctx->init_encode_params.encodeGUID, ¶ms, &val); +@@ -430,6 +644,15 @@ + NvencContext *ctx = avctx->priv_data; + int tmp, ret; + ++#if CONFIG_AV1_NVENC_ENCODER ++ if (avctx->codec->id == AV_CODEC_ID_AV1 && !NVENC_API_AT_LEAST(ctx, 12, 0)) { ++ av_log(avctx, AV_LOG_ERROR, "AV1 encoding needs nvenc API 12.0, but the driver " ++ "only supports %d.%d. Driver 520.56.06 or newer is required for AV1.\n", ++ ctx->api_version >> 4, ctx->api_version & 0xf); ++ return AVERROR(ENOSYS); ++ } ++#endif ++ + ret = nvenc_check_codec_support(avctx); + if (ret < 0) { + av_log(avctx, AV_LOG_WARNING, "Codec not supported\n"); +@@ -1088,7 +1311,10 @@ + ctx->encode_config.rcParams.lookaheadDepth, ctx->rc_lookahead); + + #ifdef NVENC_HAVE_LOOKAHEAD_LEVEL +- if (ctx->lookahead_level >= 0) { ++ if (ctx->lookahead_level >= 0 && !NVENC_API_AT_LEAST(ctx, 12, 2)) { ++ av_log(avctx, AV_LOG_WARNING, "Lookahead level needs nvenc API 12.2 " ++ "(driver 550.54.14+), ignoring.\n"); ++ } else if (ctx->lookahead_level >= 0) { + switch (ctx->lookahead_level) { + case NV_ENC_LOOKAHEAD_LEVEL_0: + case NV_ENC_LOOKAHEAD_LEVEL_1: +@@ -1274,8 +1500,12 @@ + h264->level = ctx->level; + + #ifdef NVENC_HAVE_NEW_BIT_DEPTH_API +- h264->inputBitDepth = IS_10BIT(ctx->data_pix_fmt) ? NV_ENC_BIT_DEPTH_10 : NV_ENC_BIT_DEPTH_8; +- h264->outputBitDepth = (IS_10BIT(ctx->data_pix_fmt) || ctx->highbitdepth) ? NV_ENC_BIT_DEPTH_10 : NV_ENC_BIT_DEPTH_8; ++ /* Pre-12.2 drivers read reserved space at these offsets; H264 on such ++ * drivers is always 8 bit (10 bit was rejected by the caps check). */ ++ if (NVENC_API_AT_LEAST(ctx, 12, 2)) { ++ h264->inputBitDepth = IS_10BIT(ctx->data_pix_fmt) ? NV_ENC_BIT_DEPTH_10 : NV_ENC_BIT_DEPTH_8; ++ h264->outputBitDepth = (IS_10BIT(ctx->data_pix_fmt) || ctx->highbitdepth) ? NV_ENC_BIT_DEPTH_10 : NV_ENC_BIT_DEPTH_8; ++ } + #endif + + if (ctx->coder >= 0) +@@ -1307,6 +1537,13 @@ + #endif + + #ifdef NVENC_HAVE_TIME_CODE ++ /* On by default; quietly drop it on old drivers like a pre-12.2 build ++ * (this also disables the per-frame timeCode fill). */ ++ if (ctx->s12m_tc && !NVENC_API_AT_LEAST(ctx, 12, 2)) { ++ av_log(avctx, AV_LOG_VERBOSE, "S12M timecodes need nvenc API 12.2, disabling.\n"); ++ ctx->s12m_tc = 0; ++ } ++ + if (ctx->s12m_tc) + h264->enableTimeCode = 1; + #endif +@@ -1358,18 +1595,21 @@ + hevc->intraRefreshCnt = cc->gopLength - 1; + cc->gopLength = NVENC_INFINITE_GOPLENGTH; + #ifdef NVENC_HAVE_HEVC_OUTPUT_RECOVERY_POINT_SEI +- hevc->outputRecoveryPointSEI = 1; ++ if (NVENC_API_AT_LEAST(ctx, 12, 0)) ++ hevc->outputRecoveryPointSEI = 1; + #endif + hevc->singleSliceIntraRefresh = ctx->single_slice_intra_refresh; + } + + #ifdef NVENC_HAVE_HEVC_AND_AV1_MASTERING_METADATA +- ctx->mdm = hevc->outputMasteringDisplay = !!av_frame_side_data_get(avctx->decoded_side_data, +- avctx->nb_decoded_side_data, +- AV_FRAME_DATA_MASTERING_DISPLAY_METADATA); +- ctx->cll = hevc->outputMaxCll = !!av_frame_side_data_get(avctx->decoded_side_data, +- avctx->nb_decoded_side_data, +- AV_FRAME_DATA_CONTENT_LIGHT_LEVEL); ++ if (NVENC_API_AT_LEAST(ctx, 13, 0)) { ++ ctx->mdm = hevc->outputMasteringDisplay = !!av_frame_side_data_get(avctx->decoded_side_data, ++ avctx->nb_decoded_side_data, ++ AV_FRAME_DATA_MASTERING_DISPLAY_METADATA); ++ ctx->cll = hevc->outputMaxCll = !!av_frame_side_data_get(avctx->decoded_side_data, ++ avctx->nb_decoded_side_data, ++ AV_FRAME_DATA_CONTENT_LIGHT_LEVEL); ++ } + #endif + + if (ctx->constrained_encoding) +@@ -1464,8 +1704,18 @@ + hevc->chromaFormatIDC = IS_YUV444(ctx->data_pix_fmt) ? 3 : IS_YUV422(ctx->data_pix_fmt) ? 2 : 1; + + #ifdef NVENC_HAVE_NEW_BIT_DEPTH_API +- hevc->inputBitDepth = IS_10BIT(ctx->data_pix_fmt) ? NV_ENC_BIT_DEPTH_10 : NV_ENC_BIT_DEPTH_8; +- hevc->outputBitDepth = (IS_10BIT(ctx->data_pix_fmt) || ctx->highbitdepth) ? NV_ENC_BIT_DEPTH_10 : NV_ENC_BIT_DEPTH_8; ++ if (NVENC_API_AT_LEAST(ctx, 12, 2)) { ++ hevc->inputBitDepth = IS_10BIT(ctx->data_pix_fmt) ? NV_ENC_BIT_DEPTH_10 : NV_ENC_BIT_DEPTH_8; ++ hevc->outputBitDepth = (IS_10BIT(ctx->data_pix_fmt) || ctx->highbitdepth) ? NV_ENC_BIT_DEPTH_10 : NV_ENC_BIT_DEPTH_8; ++ } else { ++ if (ctx->highbitdepth && !IS_10BIT(ctx->data_pix_fmt)) { ++ av_log(avctx, AV_LOG_ERROR, "8 -> 10 bit HEVC encoding needs nvenc API 12.2 " ++ "(driver 550.54.14+).\n"); ++ return AVERROR(ENOSYS); ++ } ++ // The old pixelBitDepthMinus8 bitfield read by pre-12.2 drivers ++ nvenc_set_legacy_hevc_bit_depth(hevc, IS_10BIT(ctx->data_pix_fmt) ? 2 : 0); ++ } + #else + hevc->pixelBitDepthMinus8 = IS_10BIT(ctx->data_pix_fmt) ? 2 : 0; + #endif +@@ -1569,20 +1819,28 @@ + av1->chromaFormatIDC = IS_YUV444(ctx->data_pix_fmt) ? 3 : 1; + + #ifdef NVENC_HAVE_NEW_BIT_DEPTH_API +- av1->inputBitDepth = IS_10BIT(ctx->data_pix_fmt) ? NV_ENC_BIT_DEPTH_10 : NV_ENC_BIT_DEPTH_8; +- av1->outputBitDepth = (IS_10BIT(ctx->data_pix_fmt) || ctx->highbitdepth) ? NV_ENC_BIT_DEPTH_10 : NV_ENC_BIT_DEPTH_8; ++ if (NVENC_API_AT_LEAST(ctx, 12, 2)) { ++ av1->inputBitDepth = IS_10BIT(ctx->data_pix_fmt) ? NV_ENC_BIT_DEPTH_10 : NV_ENC_BIT_DEPTH_8; ++ av1->outputBitDepth = (IS_10BIT(ctx->data_pix_fmt) || ctx->highbitdepth) ? NV_ENC_BIT_DEPTH_10 : NV_ENC_BIT_DEPTH_8; ++ } else { ++ // The old bitfields read by 12.0/12.1 drivers ++ nvenc_set_legacy_av1_bit_depth(av1, IS_10BIT(ctx->data_pix_fmt) ? 2 : 0, ++ (IS_10BIT(ctx->data_pix_fmt) || ctx->highbitdepth) ? 2 : 0); ++ } + #else + av1->inputPixelBitDepthMinus8 = IS_10BIT(ctx->data_pix_fmt) ? 2 : 0; + av1->pixelBitDepthMinus8 = (IS_10BIT(ctx->data_pix_fmt) || ctx->highbitdepth) ? 2 : 0; + #endif + + #ifdef NVENC_HAVE_HEVC_AND_AV1_MASTERING_METADATA +- ctx->mdm = av1->outputMasteringDisplay = !!av_frame_side_data_get(avctx->decoded_side_data, +- avctx->nb_decoded_side_data, +- AV_FRAME_DATA_MASTERING_DISPLAY_METADATA); +- ctx->cll = av1->outputMaxCll = !!av_frame_side_data_get(avctx->decoded_side_data, +- avctx->nb_decoded_side_data, +- AV_FRAME_DATA_CONTENT_LIGHT_LEVEL); ++ if (NVENC_API_AT_LEAST(ctx, 13, 0)) { ++ ctx->mdm = av1->outputMasteringDisplay = !!av_frame_side_data_get(avctx->decoded_side_data, ++ avctx->nb_decoded_side_data, ++ AV_FRAME_DATA_MASTERING_DISPLAY_METADATA); ++ ctx->cll = av1->outputMaxCll = !!av_frame_side_data_get(avctx->decoded_side_data, ++ avctx->nb_decoded_side_data, ++ AV_FRAME_DATA_CONTENT_LIGHT_LEVEL); ++ } + #endif + + if (ctx->b_ref_mode >= 0) +@@ -1676,16 +1934,16 @@ + int res = 0; + int dw, dh; + +- ctx->encode_config.version = NV_ENC_CONFIG_VER; +- ctx->init_encode_params.version = NV_ENC_INITIALIZE_PARAMS_VER; ++ ctx->encode_config.version = ctx->api_vers->config; ++ ctx->init_encode_params.version = ctx->api_vers->initialize_params; + + ctx->init_encode_params.encodeHeight = avctx->height; + ctx->init_encode_params.encodeWidth = avctx->width; + + ctx->init_encode_params.encodeConfig = &ctx->encode_config; + +- preset_config.version = NV_ENC_PRESET_CONFIG_VER; +- preset_config.presetCfg.version = NV_ENC_CONFIG_VER; ++ preset_config.version = ctx->api_vers->preset_config; ++ preset_config.presetCfg.version = ctx->api_vers->config; + + ctx->init_encode_params.tuningInfo = ctx->tuning_info; + +@@ -1694,6 +1952,16 @@ + else if (ctx->flags & NVENC_LOWLATENCY) + ctx->init_encode_params.tuningInfo = NV_ENC_TUNING_INFO_LOW_LATENCY; + ++#ifdef NVENC_HAVE_UHQ_TUNING ++ if (ctx->init_encode_params.tuningInfo == NV_ENC_TUNING_INFO_ULTRA_HIGH_QUALITY && ++ (!NVENC_API_AT_LEAST(ctx, 12, 2) || ++ (avctx->codec->id == AV_CODEC_ID_AV1 && !NVENC_API_AT_LEAST(ctx, 13, 0)))) { ++ av_log(avctx, AV_LOG_ERROR, "UHQ tuning needs nvenc API 12.2 (driver 550.54.14+), " ++ "13.0 (driver 570+) for AV1.\n"); ++ return AVERROR(ENOSYS); ++ } ++#endif ++ + nv_status = p_nvenc->nvEncGetEncodePresetConfigEx(ctx->nvencoder, + ctx->init_encode_params.encodeGUID, + ctx->init_encode_params.presetGUID, +@@ -1704,7 +1972,7 @@ + + memcpy(&ctx->encode_config, &preset_config.presetCfg, sizeof(ctx->encode_config)); + +- ctx->encode_config.version = NV_ENC_CONFIG_VER; ++ ctx->encode_config.version = ctx->api_vers->config; + + compute_dar(avctx, &dw, &dh); + ctx->init_encode_params.darHeight = dh; +@@ -1735,11 +2003,16 @@ + ctx->init_encode_params.enableWeightedPrediction = 1; + + #ifdef NVENC_HAVE_SPLIT_FRAME_ENCODING +- ctx->init_encode_params.splitEncodeMode = ctx->split_encode_mode; ++ if (NVENC_API_AT_LEAST(ctx, 12, 1)) { ++ ctx->init_encode_params.splitEncodeMode = ctx->split_encode_mode; + +- if (ctx->split_encode_mode != NV_ENC_SPLIT_DISABLE_MODE) { +- if (avctx->codec->id == AV_CODEC_ID_HEVC && ctx->weighted_pred == 1) +- av_log(avctx, AV_LOG_WARNING, "Split encoding not supported with weighted prediction enabled.\n"); ++ if (ctx->split_encode_mode != NV_ENC_SPLIT_DISABLE_MODE) { ++ if (avctx->codec->id == AV_CODEC_ID_HEVC && ctx->weighted_pred == 1) ++ av_log(avctx, AV_LOG_WARNING, "Split encoding not supported with weighted prediction enabled.\n"); ++ } ++ } else if (ctx->split_encode_mode != NV_ENC_SPLIT_AUTO_MODE) { ++ av_log(avctx, AV_LOG_WARNING, "Split frame encoding needs nvenc API 12.1 " ++ "(driver 530.41.03+), ignoring.\n"); + } + #endif + +@@ -1881,7 +2154,7 @@ + + NVENCSTATUS nv_status; + NV_ENC_CREATE_BITSTREAM_BUFFER allocOut = { 0 }; +- allocOut.version = NV_ENC_CREATE_BITSTREAM_BUFFER_VER; ++ allocOut.version = ctx->api_vers->create_bitstream_buffer; + + if (avctx->pix_fmt == AV_PIX_FMT_CUDA || avctx->pix_fmt == AV_PIX_FMT_D3D11) { + ctx->surfaces[idx].in_ref = av_frame_alloc(); +@@ -1897,7 +2170,7 @@ + return AVERROR(EINVAL); + } + +- allocSurf.version = NV_ENC_CREATE_INPUT_BUFFER_VER; ++ allocSurf.version = ctx->api_vers->create_input_buffer; + allocSurf.width = avctx->width; + allocSurf.height = avctx->height; + allocSurf.bufferFmt = ctx->surfaces[idx].format; +@@ -1985,7 +2258,7 @@ + char tmpHeader[NV_MAX_SEQ_HDR_LEN]; + + NV_ENC_SEQUENCE_PARAM_PAYLOAD payload = { 0 }; +- payload.version = NV_ENC_SEQUENCE_PARAM_PAYLOAD_VER; ++ payload.version = ctx->api_vers->sequence_param_payload; + + payload.spsppsBuffer = tmpHeader; + payload.inBufferSize = sizeof(tmpHeader); +@@ -2017,7 +2290,7 @@ + + /* the encoder has to be flushed before it can be closed */ + if (ctx->nvencoder) { +- NV_ENC_PIC_PARAMS params = { .version = NV_ENC_PIC_PARAMS_VER, ++ NV_ENC_PIC_PARAMS params = { .version = ctx->api_vers->pic_params, + .encodePicFlags = NV_ENC_PIC_FLAG_EOS }; + + res = nvenc_push_context(avctx); +@@ -2241,7 +2514,7 @@ + if (idx < 0) + return idx; + +- reg.version = NV_ENC_REGISTER_RESOURCE_VER; ++ reg.version = ctx->api_vers->register_resource; + reg.width = frames_ctx->width; + reg.height = frames_ctx->height; + reg.pitch = frame->linesize[0]; +@@ -2296,7 +2569,7 @@ + return res; + + if (!ctx->registered_frames[reg_idx].mapped) { +- ctx->registered_frames[reg_idx].in_map.version = NV_ENC_MAP_INPUT_RESOURCE_VER; ++ ctx->registered_frames[reg_idx].in_map.version = ctx->api_vers->map_input_resource; + ctx->registered_frames[reg_idx].in_map.registeredResource = ctx->registered_frames[reg_idx].regptr; + nv_status = p_nvenc->nvEncMapInputResource(ctx->nvencoder, &ctx->registered_frames[reg_idx].in_map); + if (nv_status != NV_ENC_SUCCESS) { +@@ -2316,7 +2589,7 @@ + } else { + NV_ENC_LOCK_INPUT_BUFFER lockBufferParams = { 0 }; + +- lockBufferParams.version = NV_ENC_LOCK_INPUT_BUFFER_VER; ++ lockBufferParams.version = ctx->api_vers->lock_input_buffer; + lockBufferParams.inputBuffer = nvenc_frame->input_surface; + + nv_status = p_nvenc->nvEncLockInputBuffer(ctx->nvencoder, &lockBufferParams); +@@ -2569,31 +2842,38 @@ + NvencDynLoadFunctions *dl_fn = &ctx->nvenc_dload_funcs; + NV_ENCODE_API_FUNCTION_LIST *p_nvenc = &dl_fn->nvenc_funcs; + +- NV_ENC_LOCK_BITSTREAM lock_params = { 0 }; ++ /* The SDK 12.1 NV_ENC_LOCK_BITSTREAM layout is 8 bytes larger than the ++ * 12.2+ layout this is compiled against; keep zeroed spare space after it ++ * for drivers negotiated to API <= 12.1. */ ++ struct { ++ NV_ENC_LOCK_BITSTREAM params; ++ uint32_t compat_pad[2]; ++ } lock_params_padded = { 0 }; ++ NV_ENC_LOCK_BITSTREAM *lock_params = &lock_params_padded.params; + NVENCSTATUS nv_status; + int res = 0; + + enum AVPictureType pict_type; + +- lock_params.version = NV_ENC_LOCK_BITSTREAM_VER; ++ lock_params->version = ctx->api_vers->lock_bitstream; + +- lock_params.doNotWait = 0; +- lock_params.outputBitstream = tmpoutsurf->output_surface; ++ lock_params->doNotWait = 0; ++ lock_params->outputBitstream = tmpoutsurf->output_surface; + +- nv_status = p_nvenc->nvEncLockBitstream(ctx->nvencoder, &lock_params); ++ nv_status = p_nvenc->nvEncLockBitstream(ctx->nvencoder, lock_params); + if (nv_status != NV_ENC_SUCCESS) { + res = nvenc_print_error(avctx, nv_status, "Failed locking bitstream buffer"); + goto error; + } + +- res = ff_get_encode_buffer(avctx, pkt, lock_params.bitstreamSizeInBytes, 0); ++ res = ff_get_encode_buffer(avctx, pkt, lock_params->bitstreamSizeInBytes, 0); + + if (res < 0) { + p_nvenc->nvEncUnlockBitstream(ctx->nvencoder, tmpoutsurf->output_surface); + goto error; + } + +- memcpy(pkt->data, lock_params.bitstreamBufferPtr, lock_params.bitstreamSizeInBytes); ++ memcpy(pkt->data, lock_params->bitstreamBufferPtr, lock_params->bitstreamSizeInBytes); + + nv_status = p_nvenc->nvEncUnlockBitstream(ctx->nvencoder, tmpoutsurf->output_surface); + if (nv_status != NV_ENC_SUCCESS) { +@@ -2620,7 +2900,7 @@ + tmpoutsurf->input_surface = NULL; + } + +- switch (lock_params.pictureType) { ++ switch (lock_params->pictureType) { + case NV_ENC_PIC_TYPE_IDR: + pkt->flags |= AV_PKT_FLAG_KEY; + av_fallthrough; +@@ -2644,13 +2924,13 @@ + } + + ff_encode_add_stats_side_data(pkt, +- (lock_params.frameAvgQP - 1) * FF_QP2LAMBDA, NULL, 0, pict_type); ++ (lock_params->frameAvgQP - 1) * FF_QP2LAMBDA, NULL, 0, pict_type); + +- res = nvenc_set_timestamp(avctx, &lock_params, pkt); ++ res = nvenc_set_timestamp(avctx, lock_params, pkt); + if (res < 0) + goto error2; + +- res = nvenc_retrieve_frame_data(avctx, &lock_params, pkt); ++ res = nvenc_retrieve_frame_data(avctx, lock_params, pkt); + if (res < 0) + goto error2; + +@@ -2793,14 +3073,21 @@ + NV_ENCODE_API_FUNCTION_LIST *p_nvenc = &ctx->nvenc_dload_funcs.nvenc_funcs; + NVENCSTATUS ret; + +- NV_ENC_RECONFIGURE_PARAMS params = { 0 }; ++ /* The pre-12.2 NV_ENC_RECONFIGURE_PARAMS layout is 8 bytes larger than ++ * the 12.2+ layout this is compiled against; keep zeroed spare space ++ * after it for drivers negotiated to API < 12.2. */ ++ struct { ++ NV_ENC_RECONFIGURE_PARAMS params; ++ uint32_t compat_pad[2]; ++ } params_padded = { 0 }; ++ NV_ENC_RECONFIGURE_PARAMS *params = ¶ms_padded.params; + int needs_reconfig = 0; + int needs_encode_config = 0; + int reconfig_bitrate = 0, reconfig_dar = 0; + int dw, dh; + +- params.version = NV_ENC_RECONFIGURE_PARAMS_VER; +- params.reInitEncodeParams = ctx->init_encode_params; ++ params->version = ctx->api_vers->reconfigure_params; ++ params->reInitEncodeParams = ctx->init_encode_params; + + compute_dar(avctx, &dw, &dh); + if (dw != ctx->init_encode_params.darWidth || dh != ctx->init_encode_params.darHeight) { +@@ -2809,47 +3096,59 @@ + ctx->init_encode_params.darWidth, + ctx->init_encode_params.darHeight, dw, dh); + +- params.reInitEncodeParams.darHeight = dh; +- params.reInitEncodeParams.darWidth = dw; ++ params->reInitEncodeParams.darHeight = dh; ++ params->reInitEncodeParams.darWidth = dw; + + needs_reconfig = 1; + reconfig_dar = 1; + } + + if (ctx->rc != NV_ENC_PARAMS_RC_CONSTQP && ctx->support_dyn_bitrate) { +- if (avctx->bit_rate > 0 && params.reInitEncodeParams.encodeConfig->rcParams.averageBitRate != avctx->bit_rate) { ++ if (avctx->bit_rate > 0 && params->reInitEncodeParams.encodeConfig->rcParams.averageBitRate != avctx->bit_rate) { + av_log(avctx, AV_LOG_VERBOSE, + "avg bitrate change: %d -> %d\n", +- params.reInitEncodeParams.encodeConfig->rcParams.averageBitRate, ++ params->reInitEncodeParams.encodeConfig->rcParams.averageBitRate, + (uint32_t)avctx->bit_rate); + +- params.reInitEncodeParams.encodeConfig->rcParams.averageBitRate = avctx->bit_rate; ++ params->reInitEncodeParams.encodeConfig->rcParams.averageBitRate = avctx->bit_rate; + reconfig_bitrate = 1; + } + + if (avctx->rc_max_rate > 0 && ctx->encode_config.rcParams.maxBitRate != avctx->rc_max_rate) { + av_log(avctx, AV_LOG_VERBOSE, + "max bitrate change: %d -> %d\n", +- params.reInitEncodeParams.encodeConfig->rcParams.maxBitRate, ++ params->reInitEncodeParams.encodeConfig->rcParams.maxBitRate, + (uint32_t)avctx->rc_max_rate); + +- params.reInitEncodeParams.encodeConfig->rcParams.maxBitRate = avctx->rc_max_rate; ++ params->reInitEncodeParams.encodeConfig->rcParams.maxBitRate = avctx->rc_max_rate; + reconfig_bitrate = 1; + } + + if (avctx->rc_buffer_size > 0 && ctx->encode_config.rcParams.vbvBufferSize != avctx->rc_buffer_size) { + av_log(avctx, AV_LOG_VERBOSE, + "vbv buffer size change: %d -> %d\n", +- params.reInitEncodeParams.encodeConfig->rcParams.vbvBufferSize, ++ params->reInitEncodeParams.encodeConfig->rcParams.vbvBufferSize, + avctx->rc_buffer_size); + +- params.reInitEncodeParams.encodeConfig->rcParams.vbvBufferSize = avctx->rc_buffer_size; ++ params->reInitEncodeParams.encodeConfig->rcParams.vbvBufferSize = avctx->rc_buffer_size; + reconfig_bitrate = 1; + } + + if (reconfig_bitrate) { +- params.resetEncoder = 1; +- params.forceIDR = 1; ++ if (NVENC_API_AT_LEAST(ctx, 12, 2)) { ++ params->resetEncoder = 1; ++ params->forceIDR = 1; ++ } else { ++ /* SDK 12.2 inserted a reserved word at offset 4 of ++ * NV_ENC_RECONFIGURE_PARAMS, shifting the trailing ++ * resetEncoder/forceIDR bits from byte 1816 to 1808. Pre-12.2 ++ * drivers read them at 1816, which is compat_pad[0] of the ++ * padded wrapper (verified against SDK 11.1-12.1 headers: ++ * resetEncoder = bit 0, forceIDR = bit 1). The compiled ++ * bitfields must stay 0: byte 1808 lies in reserved space of ++ * the embedded pre-12.2 reInitEncodeParams. */ ++ params_padded.compat_pad[0] = 0x1 | 0x2; ++ } + + needs_encode_config = 1; + needs_reconfig = 1; +@@ -2857,10 +3156,10 @@ + } + + if (!needs_encode_config) +- params.reInitEncodeParams.encodeConfig = NULL; ++ params->reInitEncodeParams.encodeConfig = NULL; + + if (needs_reconfig) { +- ret = p_nvenc->nvEncReconfigureEncoder(ctx->nvencoder, ¶ms); ++ ret = p_nvenc->nvEncReconfigureEncoder(ctx->nvencoder, params); + if (ret != NV_ENC_SUCCESS) { + nvenc_print_error(avctx, ret, "failed to reconfigure nvenc"); + } else { +@@ -2870,9 +3169,9 @@ + } + + if (reconfig_bitrate) { +- ctx->encode_config.rcParams.averageBitRate = params.reInitEncodeParams.encodeConfig->rcParams.averageBitRate; +- ctx->encode_config.rcParams.maxBitRate = params.reInitEncodeParams.encodeConfig->rcParams.maxBitRate; +- ctx->encode_config.rcParams.vbvBufferSize = params.reInitEncodeParams.encodeConfig->rcParams.vbvBufferSize; ++ ctx->encode_config.rcParams.averageBitRate = params->reInitEncodeParams.encodeConfig->rcParams.averageBitRate; ++ ctx->encode_config.rcParams.maxBitRate = params->reInitEncodeParams.encodeConfig->rcParams.maxBitRate; ++ ctx->encode_config.rcParams.vbvBufferSize = params->reInitEncodeParams.encodeConfig->rcParams.vbvBufferSize; + } + + } +@@ -2971,7 +3270,7 @@ + NV_ENCODE_API_FUNCTION_LIST *p_nvenc = &dl_fn->nvenc_funcs; + + NV_ENC_PIC_PARAMS pic_params = { 0 }; +- pic_params.version = NV_ENC_PIC_PARAMS_VER; ++ pic_params.version = ctx->api_vers->pic_params; + + if ((!ctx->cu_context && !ctx->d3d11_device) || !ctx->nvencoder) + return AVERROR(EINVAL); +--- a/libavcodec/nvenc.h ++++ b/libavcodec/nvenc.h +@@ -80,6 +80,54 @@ + #define NVENC_HAVE_AV1_HGOP_SUPPORT + #endif + ++/* ++ * Runtime NVENC API version negotiation. ++ * ++ * The NVENC driver rejects clients that present a newer API version than it ++ * supports, so an FFmpeg built against new SDK headers normally refuses to ++ * run on older drivers. Instead of failing, we query the driver's maximum ++ * supported API version at runtime and present struct version words (and the ++ * session apiVersion) matching what that driver expects, while disabling ++ * features that need a newer API. This keeps one binary working from driver ++ * 470.57.02 (API 11.1, the last driver branch for Kepler / first-generation ++ * NVENC GPUs) up to current drivers, including AV1 support on drivers that ++ * have it. ++ * ++ * The struct version table and the legacy bit-depth field positions below ++ * were derived from and verified against the exact layouts of nv-codec-headers ++ * n11.1.5.3, n12.0.16.1, n12.1.14.0, n12.2.72.0 and n13.0.19.0. All struct ++ * fields FFmpeg touches sit at identical offsets in all of those versions ++ * (new fields are only ever carved out of reserved space). Building against ++ * any other headers invalidates those assumptions, so refuse it. ++ */ ++#if !NVENCAPI_CHECK_VERSION(13, 0) || NVENCAPI_CHECK_VERSION(13, 1) ++#error "The nvenc runtime API negotiation patch requires nv-codec-headers 13.0.x. Re-verify the struct version table and legacy bit-depth field layout before moving to other headers." ++#endif ++ ++typedef struct NvencApiVersion { ++ uint32_t packed; // (major << 4) | minor, the NvEncodeAPIGetMaxSupportedVersion encoding ++ uint32_t api; // NVENCAPI_VERSION of that SDK: major | (minor << 24) ++ uint32_t function_list; // NV_ENCODE_API_FUNCTION_LIST_VER ++ uint32_t open_session_params; // NV_ENC_OPEN_ENCODE_SESSION_EX_PARAMS_VER ++ uint32_t caps_param; // NV_ENC_CAPS_PARAM_VER ++ uint32_t initialize_params; // NV_ENC_INITIALIZE_PARAMS_VER ++ uint32_t config; // NV_ENC_CONFIG_VER ++ uint32_t preset_config; // NV_ENC_PRESET_CONFIG_VER ++ uint32_t pic_params; // NV_ENC_PIC_PARAMS_VER ++ uint32_t lock_bitstream; // NV_ENC_LOCK_BITSTREAM_VER ++ uint32_t lock_input_buffer; // NV_ENC_LOCK_INPUT_BUFFER_VER ++ uint32_t map_input_resource; // NV_ENC_MAP_INPUT_RESOURCE_VER ++ uint32_t register_resource; // NV_ENC_REGISTER_RESOURCE_VER ++ uint32_t create_input_buffer; // NV_ENC_CREATE_INPUT_BUFFER_VER ++ uint32_t create_bitstream_buffer; // NV_ENC_CREATE_BITSTREAM_BUFFER_VER ++ uint32_t reconfigure_params; // NV_ENC_RECONFIGURE_PARAMS_VER ++ uint32_t sequence_param_payload; // NV_ENC_SEQUENCE_PARAM_PAYLOAD_VER ++} NvencApiVersion; ++ ++#define NVENC_API_PACKED(major, minor) (((uint32_t)(major) << 4) | (minor)) ++#define NVENC_API_AT_LEAST(ctx, major, minor) \ ++ ((ctx)->api_version >= NVENC_API_PACKED(major, minor)) ++ + typedef struct NvencSurface + { + NV_ENC_INPUT_PTR input_surface; +@@ -172,6 +220,11 @@ + NvencDynLoadFunctions nvenc_dload_funcs; + + NV_ENC_INITIALIZE_PARAMS init_encode_params; ++ /* The pre-12.2 NV_ENC_INITIALIZE_PARAMS layout is 8 bytes larger than the ++ * 12.2+ layout this is compiled against. Drivers negotiated to API < 12.2 ++ * read that larger footprint, so keep zeroed spare space directly after ++ * the struct. Must stay right after init_encode_params and stay zero. */ ++ uint32_t init_encode_params_compat_pad[2]; + NV_ENC_CONFIG encode_config; + CUcontext cu_context; + CUcontext cu_context_internal; +@@ -275,6 +328,11 @@ + int cbr_padding; + int multiview, multiview_supported; + int display_sei_sent; ++ ++ /* Runtime-negotiated NVENC API version: the newest version supported by ++ * both the driver and this build. Set in nvenc_load_libraries. */ ++ const NvencApiVersion *api_vers; ++ uint32_t api_version; // negotiated, (major << 4) | minor + } NvencContext; + + int ff_nvenc_encode_init(AVCodecContext *avctx); diff --git a/subprojects/packagefiles/ffmpeg/meson.build b/subprojects/packagefiles/ffmpeg/meson.build new file mode 100644 index 0000000..11973f1 --- /dev/null +++ b/subprojects/packagefiles/ffmpeg/meson.build @@ -0,0 +1,35 @@ +project('ffmpeg', 'c', version : '9.0') + +# Neither ffmpeg nor the libraries it depends on use meson, so they are built with their +# own build systems into a shared prefix and then exposed to gpu-screen-recorder as +# regular pkg-config dependencies. + +install_prefix = meson.current_build_dir() / 'install' +c_compiler = ' '.join(meson.get_compiler('c').cmd_array()) +build_library = find_program(meson.global_source_root() / 'extra' / 'build_ffmpeg.sh') + +# srt links against mbedtls and ffmpeg links against all of them, so the order matters. +libraries = ['nv-codec-headers', 'x264', 'opus', 'mbedtls', 'srt', 'ffmpeg'] + +foreach library : libraries + source_dir = library == 'ffmpeg' ? meson.current_source_dir() : subproject(library).get_variable('source_dir') + build_result = run_command(build_library, library, source_dir, meson.current_build_dir() / library, install_prefix, c_compiler, check : true) + if build_result.stderr().strip() != '' + message(build_result.stderr().strip()) + endif +endforeach + +ffmpeg_libs = ['libavformat', 'libavfilter', 'libavcodec', 'libswresample', 'libavutil'] + +pkgconfig = find_program('pkg-config') +pkgconfig_env = environment() +pkgconfig_env.prepend('PKG_CONFIG_PATH', install_prefix / 'lib' / 'pkgconfig') + +ffmpeg_cflags = run_command(pkgconfig, '--cflags', ffmpeg_libs, env : pkgconfig_env, check : true).stdout().split() +ffmpeg_ldflags = run_command(pkgconfig, '--static', '--libs', ffmpeg_libs, env : pkgconfig_env, check : true).stdout().split() + +ffmpeg_dep = declare_dependency(compile_args : ffmpeg_cflags, link_args : ffmpeg_ldflags) + +foreach ffmpeg_lib : ffmpeg_libs + meson.override_dependency(ffmpeg_lib, ffmpeg_dep) +endforeach diff --git a/subprojects/packagefiles/mbedtls/meson.build b/subprojects/packagefiles/mbedtls/meson.build new file mode 100644 index 0000000..802357e --- /dev/null +++ b/subprojects/packagefiles/mbedtls/meson.build @@ -0,0 +1,3 @@ +project('mbedtls') + +source_dir = meson.current_source_dir() diff --git a/subprojects/packagefiles/nv-codec-headers/meson.build b/subprojects/packagefiles/nv-codec-headers/meson.build new file mode 100644 index 0000000..cc59db7 --- /dev/null +++ b/subprojects/packagefiles/nv-codec-headers/meson.build @@ -0,0 +1,3 @@ +project('nv-codec-headers') + +source_dir = meson.current_source_dir() diff --git a/subprojects/packagefiles/opus/meson.build b/subprojects/packagefiles/opus/meson.build new file mode 100644 index 0000000..858205d --- /dev/null +++ b/subprojects/packagefiles/opus/meson.build @@ -0,0 +1,3 @@ +project('opus') + +source_dir = meson.current_source_dir() diff --git a/subprojects/packagefiles/srt/meson.build b/subprojects/packagefiles/srt/meson.build new file mode 100644 index 0000000..3fa707e --- /dev/null +++ b/subprojects/packagefiles/srt/meson.build @@ -0,0 +1,3 @@ +project('srt') + +source_dir = meson.current_source_dir() diff --git a/subprojects/packagefiles/x264/meson.build b/subprojects/packagefiles/x264/meson.build new file mode 100644 index 0000000..87f3a80 --- /dev/null +++ b/subprojects/packagefiles/x264/meson.build @@ -0,0 +1,3 @@ +project('x264') + +source_dir = meson.current_source_dir() diff --git a/subprojects/srt.wrap b/subprojects/srt.wrap new file mode 100644 index 0000000..583b2e8 --- /dev/null +++ b/subprojects/srt.wrap @@ -0,0 +1,6 @@ +[wrap-file] +directory = srt-1.5.6 +source_url = https://github.com/Haivision/srt/archive/refs/tags/v1.5.6.tar.gz +source_filename = srt-1.5.6.tar.gz +source_hash = 2c4980c2c4cfd142d21b829d939dc51db9c6628af5967fff62fd7290769569c7 +patch_directory = srt diff --git a/subprojects/x264.wrap b/subprojects/x264.wrap new file mode 100644 index 0000000..f9428cb --- /dev/null +++ b/subprojects/x264.wrap @@ -0,0 +1,6 @@ +[wrap-file] +directory = x264-b35605ace3ddf7c1a5d67a2eb553f034aef41d55 +source_url = https://code.videolan.org/videolan/x264/-/archive/b35605ace3ddf7c1a5d67a2eb553f034aef41d55/x264-b35605ace3ddf7c1a5d67a2eb553f034aef41d55.tar.bz2 +source_filename = x264-b35605ace3ddf7c1a5d67a2eb553f034aef41d55.tar.bz2 +source_hash = 6eeb82934e69fd51e043bd8c5b0d152839638d1ce7aa4eea65a3fedcf83ff224 +patch_directory = x264 diff --git a/tools/gsr-cli/main.c b/tools/gsr-cli/main.c new file mode 100644 index 0000000..fbfc2fa --- /dev/null +++ b/tools/gsr-cli/main.c @@ -0,0 +1,364 @@ +#include "../../include/json.h" +#include "../../include/log.h" + +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <errno.h> +#include <inttypes.h> +#include <limits.h> +#include <unistd.h> +#include <sys/socket.h> +#include <sys/time.h> +#include <sys/un.h> + +#define GSR_CLI_REQUEST_ID 1 +#define GSR_CLI_MAX_REQUEST_SIZE 256 +#define GSR_CLI_MAX_REPLY_SIZE 8192 +#define GSR_CLI_REPLY_TIMEOUT_SECONDS 10 +#define GSR_CLI_NO_REPLY_TIMEOUT 0 + +static void usage(void) { + printf("usage: gsr-cli -ipc <socket_path> <command> [command_arguments...]\n"); + printf("\n"); + printf("Sends a command to a GPU Screen Recorder instance that was started with the -ipc option.\n"); + printf("\n"); + printf("OPTIONS:\n"); + printf(" -ipc <socket_path>\n"); + printf(" The unix domain socket that GPU Screen Recorder was started with. Required.\n"); + printf("\n"); + printf("COMMANDS:\n"); + printf(" status\n"); + printf(" Check if a GPU Screen Recorder instance is listening on the socket. Prints \"running\" or\n"); + printf(" \"not running\" and exits with 0 when it's running.\n"); + printf(" stop\n"); + printf(" Stop and save the recording (stop without save in replay mode). Waits until the recording\n"); + printf(" has been saved and prints the path of the saved file.\n"); + printf(" toggle-pause\n"); + printf(" Pause/unpause the recording (not for streaming/replay).\n"); + printf(" set-paused true|false\n"); + printf(" Pause/unpause the recording (not for streaming/replay). Unlike toggle-pause this doesn't\n"); + printf(" fail when the recording is already paused/unpaused.\n"); + printf(" toggle-replay-recording\n"); + printf(" Start/stop a regular recording during replay/streaming.\n"); + printf(" start-replay-recording\n"); + printf(" Start a regular recording during replay/streaming. Does nothing when a recording is already running.\n"); + printf(" stop-replay-recording\n"); + printf(" Stop the regular recording that runs during replay/streaming. Waits until the recording\n"); + printf(" has been saved and prints the path of the saved file.\n"); + printf(" save-replay [seconds] [restart-replay=true|false]\n"); + printf(" Save the replay. The number of seconds has to be larger than 0. The whole replay buffer is\n"); + printf(" saved when no number of seconds is given. Waits until the replay has been saved and prints\n"); + printf(" the path of the saved file. restart-replay overrides the -restart-replay-on-save option\n"); + printf(" of GPU Screen Recorder for this save.\n"); + printf("\n"); + printf("EXAMPLES:\n"); + printf(" gsr-cli -ipc \"$XDG_RUNTIME_DIR/gsr.sock\" status\n"); + printf(" gsr-cli -ipc \"$XDG_RUNTIME_DIR/gsr.sock\" save-replay 30\n"); + printf(" gsr-cli -ipc \"$XDG_RUNTIME_DIR/gsr.sock\" save-replay restart-replay=true\n"); + fflush(stdout); +} + +static bool string_to_int64(const char *str, int64_t *result) { + char *number_end = NULL; + errno = 0; + const long long parsed_value = strtoll(str, &number_end, 10); + if(errno != 0 || number_end == str || *number_end != '\0') + return false; + + *result = parsed_value; + return true; +} + +/* Returns the socket, or -1 on failure. Only logs an error when the failure isn't a missing GPU Screen Recorder instance */ +static int ipc_connect(const char *socket_filepath, int reply_timeout_seconds) { + struct sockaddr_un addr; + memset(&addr, 0, sizeof(addr)); + addr.sun_family = AF_UNIX; + if(snprintf(addr.sun_path, sizeof(addr.sun_path), "%s", socket_filepath) >= (int)sizeof(addr.sun_path)) { + gsr_log(GSR_LOG_LEVEL_ERROR, "the ipc socket path is too long, it can be at most %d characters: \"%s\"", (int)sizeof(addr.sun_path) - 1, socket_filepath); + return -1; + } + + const int fd = socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0); + if(fd == -1) { + gsr_log(GSR_LOG_LEVEL_ERROR, "failed to create a socket, error: %s", strerror(errno)); + return -1; + } + + struct timeval timeout; + timeout.tv_sec = reply_timeout_seconds; + timeout.tv_usec = 0; + if(reply_timeout_seconds != GSR_CLI_NO_REPLY_TIMEOUT) + setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout)); + + timeout.tv_sec = GSR_CLI_REPLY_TIMEOUT_SECONDS; + setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &timeout, sizeof(timeout)); + + if(connect(fd, (const struct sockaddr*)&addr, sizeof(addr)) == -1) { + close(fd); + return -1; + } + + return fd; +} + +static bool ipc_send_all(int fd, const char *data, size_t size) { + size_t offset = 0; + while(offset < size) { + const ssize_t bytes_written = send(fd, data + offset, size - offset, MSG_NOSIGNAL); + if(bytes_written > 0) { + offset += bytes_written; + continue; + } + + if(bytes_written == -1 && errno == EINTR) + continue; + + gsr_log(GSR_LOG_LEVEL_ERROR, "failed to send the request, error: %s", strerror(errno)); + return false; + } + return true; +} + +/* Reads until a newline. |reply_size| is set to the size of the reply, excluding the newline */ +static bool ipc_receive_reply(int fd, char *reply, size_t reply_capacity, size_t *reply_size) { + size_t offset = 0; + for(;;) { + if(offset == reply_capacity) { + gsr_log(GSR_LOG_LEVEL_ERROR, "the reply is too large"); + return false; + } + + const ssize_t bytes_read = recv(fd, reply + offset, reply_capacity - offset, 0); + if(bytes_read == 0) { + gsr_log(GSR_LOG_LEVEL_ERROR, "GPU Screen Recorder closed the connection before replying"); + return false; + } + + if(bytes_read == -1) { + if(errno == EINTR) + continue; + + if(errno == EAGAIN || errno == EWOULDBLOCK) + gsr_log(GSR_LOG_LEVEL_ERROR, "timed out after %d seconds waiting for a reply", GSR_CLI_REPLY_TIMEOUT_SECONDS); + else + gsr_log(GSR_LOG_LEVEL_ERROR, "failed to receive the reply, error: %s", strerror(errno)); + return false; + } + + const char *newline = memchr(reply + offset, '\n', bytes_read); + offset += bytes_read; + if(newline) { + *reply_size = newline - reply; + return true; + } + } +} + +static void print_json_string(const char *str, size_t size) { + for(size_t i = 0; i < size; ++i) { + char c = str[i]; + if(c == '\\' && i + 1 < size) { + ++i; + switch(str[i]) { + case 'n': c = '\n'; break; + case 'r': c = '\r'; break; + case 't': c = '\t'; break; + default: c = str[i]; break; + } + } + putchar(c); + } + putchar('\n'); +} + +/* Returns the exit code that gsr-cli should exit with */ +static int ipc_handle_reply(char *reply, size_t reply_size, int64_t request_id) { + sj_Reader reader = sj_reader(reply, reply_size); + const sj_Value root = sj_read(&reader); + if(root.type != SJ_OBJECT) { + gsr_log(GSR_LOG_LEVEL_ERROR, "expected the reply to be a json object, got: %.*s", (int)reply_size, reply); + return 1; + } + + int64_t id = 0; + bool has_id = false; + sj_Value result_value; + bool has_result = false; + sj_Value data_value; + bool has_data = false; + + sj_Value key; + sj_Value value; + while(sj_iter_object(&reader, root, &key, &value)) { + if(gsr_json_string_equals(&key, "id")) { + has_id = gsr_json_number_to_int64(&value, &id); + } else if(gsr_json_string_equals(&key, "result")) { + result_value = value; + has_result = value.type == SJ_STRING; + } else if(gsr_json_string_equals(&key, "data")) { + data_value = value; + has_data = true; + } + } + + if(reader.error) { + gsr_log(GSR_LOG_LEVEL_ERROR, "failed to parse the reply: %s", reader.error); + return 1; + } + + if(!has_id || id != request_id) { + gsr_log(GSR_LOG_LEVEL_ERROR, "received a reply to another request: %.*s", (int)reply_size, reply); + return 1; + } + + if(!has_result) { + gsr_log(GSR_LOG_LEVEL_ERROR, "the reply is missing the 'result' field: %.*s", (int)reply_size, reply); + return 1; + } + + if(gsr_json_string_equals(&result_value, "ok")) { + if(has_data && data_value.type == SJ_STRING) + print_json_string(data_value.start, data_value.end - data_value.start); + return 0; + } + + if(has_data && data_value.type == SJ_STRING) + gsr_log(GSR_LOG_LEVEL_ERROR, "%.*s", (int)(data_value.end - data_value.start), data_value.start); + else + gsr_log(GSR_LOG_LEVEL_ERROR, "the request failed: %.*s", (int)reply_size, reply); + + return 1; +} + +static int status_command(const char *socket_filepath) { + const int fd = ipc_connect(socket_filepath, GSR_CLI_REPLY_TIMEOUT_SECONDS); + if(fd == -1) { + printf("not running\n"); + fflush(stdout); + return 1; + } + + close(fd); + printf("running\n"); + fflush(stdout); + return 0; +} + +static int send_request(const char *socket_filepath, const char *request, int reply_timeout_seconds) { + const int fd = ipc_connect(socket_filepath, reply_timeout_seconds); + if(fd == -1) { + gsr_log(GSR_LOG_LEVEL_ERROR, "failed to connect to \"%s\". Is GPU Screen Recorder running with the -ipc option?", socket_filepath); + return 1; + } + + int exit_code = 1; + char reply[GSR_CLI_MAX_REPLY_SIZE]; + size_t reply_size = 0; + if(ipc_send_all(fd, request, strlen(request)) && ipc_receive_reply(fd, reply, sizeof(reply), &reply_size)) + exit_code = ipc_handle_reply(reply, reply_size, GSR_CLI_REQUEST_ID); + + close(fd); + return exit_code; +} + +int main(int argc, char **argv) { + if(argc == 2 && (strcmp(argv[1], "-h") == 0 || strcmp(argv[1], "--help") == 0)) { + usage(); + return 0; + } + + if(argc < 4 || strcmp(argv[1], "-ipc") != 0) { + usage(); + return 1; + } + + if(argc > 6) { + gsr_log(GSR_LOG_LEVEL_ERROR, "too many arguments"); + usage(); + return 1; + } + + const char *socket_filepath = argv[2]; + const char *command = argv[3]; + const char *command_argument = argc >= 5 ? argv[4] : NULL; + const char *command_argument2 = argc >= 6 ? argv[5] : NULL; + char request[GSR_CLI_MAX_REQUEST_SIZE]; + + if(strcmp(command, "save-replay") == 0) { + bool has_seconds = false; + int64_t seconds = 0; + const char *restart_replay = NULL; + + const char *command_arguments[2] = { command_argument, command_argument2 }; + for(int i = 0; i < 2; ++i) { + const char *argument = command_arguments[i]; + if(!argument) + continue; + + if(strncmp(argument, "restart-replay=", 15) == 0) { + restart_replay = argument + 15; + if(strcmp(restart_replay, "true") != 0 && strcmp(restart_replay, "false") != 0) { + gsr_log(GSR_LOG_LEVEL_ERROR, "expected restart-replay to be either true or false, got: '%s'", restart_replay); + return 1; + } + } else if(!has_seconds && string_to_int64(argument, &seconds) && seconds > 0 && seconds <= INT_MAX) { + has_seconds = true; + } else { + gsr_log(GSR_LOG_LEVEL_ERROR, "expected the argument to be the number of seconds to save (an integer larger than 0) or restart-replay=true|false, got: '%s'", argument); + return 1; + } + } + + if(restart_replay && has_seconds) + snprintf(request, sizeof(request), "{\"id\":%d,\"name\":\"save-replay\",\"data\":{\"seconds\":%" PRIi64 ",\"restart-replay\":%s}}\n", GSR_CLI_REQUEST_ID, seconds, restart_replay); + else if(restart_replay) + snprintf(request, sizeof(request), "{\"id\":%d,\"name\":\"save-replay\",\"data\":{\"restart-replay\":%s}}\n", GSR_CLI_REQUEST_ID, restart_replay); + else if(has_seconds) + snprintf(request, sizeof(request), "{\"id\":%d,\"name\":\"save-replay\",\"data\":{\"seconds\":%" PRIi64 "}}\n", GSR_CLI_REQUEST_ID, seconds); + else + snprintf(request, sizeof(request), "{\"id\":%d,\"name\":\"save-replay\"}\n", GSR_CLI_REQUEST_ID); + + return send_request(socket_filepath, request, GSR_CLI_NO_REPLY_TIMEOUT); + } + + if(command_argument2) { + gsr_log(GSR_LOG_LEVEL_ERROR, "the '%s' command doesn't take more than one argument", command); + usage(); + return 1; + } + + if(strcmp(command, "set-paused") == 0) { + if(!command_argument || (strcmp(command_argument, "true") != 0 && strcmp(command_argument, "false") != 0)) { + gsr_log(GSR_LOG_LEVEL_ERROR, "the 'set-paused' command expects either true or false as the argument"); + usage(); + return 1; + } + snprintf(request, sizeof(request), "{\"id\":%d,\"name\":\"set-paused\",\"data\":%s}\n", GSR_CLI_REQUEST_ID, command_argument); + return send_request(socket_filepath, request, GSR_CLI_REPLY_TIMEOUT_SECONDS); + } + + if(command_argument) { + gsr_log(GSR_LOG_LEVEL_ERROR, "the '%s' command doesn't take an argument", command); + usage(); + return 1; + } + + if(strcmp(command, "status") == 0) + return status_command(socket_filepath); + + if(strcmp(command, "toggle-pause") == 0 || strcmp(command, "toggle-replay-recording") == 0 || strcmp(command, "start-replay-recording") == 0) { + snprintf(request, sizeof(request), "{\"id\":%d,\"name\":\"%s\"}\n", GSR_CLI_REQUEST_ID, command); + return send_request(socket_filepath, request, GSR_CLI_REPLY_TIMEOUT_SECONDS); + } + + if(strcmp(command, "stop") == 0 || strcmp(command, "stop-replay-recording") == 0) { + snprintf(request, sizeof(request), "{\"id\":%d,\"name\":\"%s\"}\n", GSR_CLI_REQUEST_ID, command); + return send_request(socket_filepath, request, GSR_CLI_NO_REPLY_TIMEOUT); + } + + gsr_log(GSR_LOG_LEVEL_ERROR, "invalid command '%s'", command); + usage(); + return 1; +} |
