From cf50b5a377d759fc5705d0d36276d393b46fa90e Mon Sep 17 00:00:00 2001 From: klein panic Date: Sun, 13 Apr 2025 02:27:22 -0400 Subject: [PATCH] initial commit --- .gitignore | 2 + LICENSE | 21 ++ Makefile | 29 +++ README.md | 125 +++++++++++ TODO.md | 61 ++++++ ceras | Bin 0 -> 61440 bytes include/audio.h | 37 ++++ include/config.h | 15 ++ include/debug.h | 16 ++ include/encoder.h | 67 ++++++ include/gui.h | 79 +++++++ include/recorder.h | 57 +++++ include/version.h | 7 + src/audio.c | 69 ++++++ src/encoder.c | 358 +++++++++++++++++++++++++++++++ src/gui.c | 285 +++++++++++++++++++++++++ src/main.c | 518 +++++++++++++++++++++++++++++++++++++++++++++ src/recorder.c | 174 +++++++++++++++ 18 files changed, 1920 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 Makefile create mode 100644 README.md create mode 100644 TODO.md create mode 100755 ceras create mode 100644 include/audio.h create mode 100644 include/config.h create mode 100644 include/debug.h create mode 100644 include/encoder.h create mode 100644 include/gui.h create mode 100644 include/recorder.h create mode 100644 include/version.h create mode 100644 src/audio.c create mode 100644 src/encoder.c create mode 100644 src/gui.c create mode 100644 src/main.c create mode 100644 src/recorder.c diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fe24c5b --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +obj/ +CERAS diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..16f1299 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 kleinpanic + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..03f14e4 --- /dev/null +++ b/Makefile @@ -0,0 +1,29 @@ +CC = gcc +PKG_CONFIG = pkg-config +CFLAGS = -Wall -O2 -pthread `$(PKG_CONFIG) --cflags gtk+-3.0 x11 alsa` +LDFLAGS = -lX11 -lXrandr -lasound \ + -lavformat -lavcodec -lavutil -lswscale -lavdevice -lswresample \ + `$(PKG_CONFIG) --libs gtk+-3.0` + +SRCDIR = src +OBJDIR = obj +INCDIR = include + +SOURCES = $(wildcard $(SRCDIR)/*.c) +OBJECTS = $(patsubst $(SRCDIR)/%.c,$(OBJDIR)/%.o,$(SOURCES)) +TARGET = ceras + +all: $(TARGET) + +$(TARGET): $(OBJDIR) $(OBJECTS) + $(CC) -o $@ $(OBJECTS) $(LDFLAGS) + +$(OBJDIR): + mkdir -p $@ + +$(OBJDIR)/%.o: $(SRCDIR)/%.c | $(OBJDIR) + $(CC) -c -o $@ $< -I$(INCDIR) $(CFLAGS) + +clean: + rm -rf $(OBJDIR) $(TARGET) + diff --git a/README.md b/README.md new file mode 100644 index 0000000..1794ac1 --- /dev/null +++ b/README.md @@ -0,0 +1,125 @@ +# CERES - Capture Encode Record and Export Screen + +## Overview + +**CERES** is a Linux-based screen recorder that captures video (and optionally audio) from your screen using X11 and ALSA. It also supports webcam preview and capture. The program uses FFmpeg libraries to encode the captured video and audio into a single output file and GTK+3 to provide a graphical user interface (GUI) for user interaction. + +## Code Architecture & Review + +The code is organized into several modular components: + +- **GUI Module (gui.c / gui.h):** + This module creates and manages the user interface using GTK+3. It provides controls for: + - Selecting the recording source (full screen, specific window, or specific monitor). + - Choosing recording quality, resolution, and frame rate (FPS). + - Selecting the audio codec (AAC, PCM, or Opus). + - Toggling audio capture and webcam preview. + - Displaying real-time information (elapsed recording time, file size, and output filename). + +- **Screen Capture Module (recorder.c / recorder.h):** + This component is responsible for capturing the screen (or a specific window) using X11 functions. It supports: + - Interactive window selection (via pointer grab and click). + - Full-screen capture with monitor geometry obtained via XRandR. + - Dynamic updates of window geometry when capturing a window. + +- **Audio Capture Module (audio.c / audio.h):** + This module uses the ALSA library to capture audio from the system’s default PCM device. It allows: + - Starting and stopping audio capture. + - Dynamically toggling audio capture during recording. + +- **Encoding Module (encoder.c / encoder.h):** + FFmpeg libraries are used for encoding both video and audio streams. In this module: + - Video is encoded using H.264. + - Audio can be encoded using AAC, PCM (lossless), or Opus. + - The encoded file is saved to `~/Videos/Screenrecords/` with an autogenerated name (which can be renamed after recording). + +- **Main Application (main.c):** + The main file initializes GTK+3, creates the GUI, and sets up the various threads for screen capture, audio capture, and webcam preview. It also includes command-line processing for additional options: + - `--help` prints a help message. + - `--version` prints version information. + - `--debug` enables more verbose debug output during execution. + +## Dependencies + +To build and run **CERES**, ensure that the following dependencies are installed on your system: + +- **GTK+ 3.0:** for the GUI components + (Package: `libgtk-3-dev` on Debian/Ubuntu) + +- **FFmpeg Libraries:** including libavcodec, libavformat, libavutil, libswscale, libavdevice, libswresample + (Packages: `libavcodec-dev`, `libavformat-dev`, `libavutil-dev`, `libswscale-dev`, `libavdevice-dev`, `libswresample-dev`) + +- **ALSA Library:** for audio capture + (Package: `libasound2-dev`) + +- **X11 and XRandR:** for screen capture and monitor geometry + (Packages: `libx11-dev`, `libxrandr-dev`) + +## Build Instructions + +The project comes with a Makefile that automatically compiles all source files and links the required libraries. To build the project, follow these steps: + +1. Install the necessary dependencies. For example, on Debian/Ubuntu run: + ```bash + sudo apt-get install libgtk-3-dev libavcodec-dev libavformat-dev libavutil-dev libswscale-dev libavdevice-dev libswresample-dev libasound2-dev libx11-dev libxrandr-dev + ``` + +2. In the project root directory, run: + ```bash + make + ``` + This will compile the sources in the `src/` directory, output object files into the `obj/` directory, and create the executable named `screen_recorder`. + +3. To clean up object files and the executable, run: + ```bash + make clean + ``` + +## Usage Instructions + +After building the application, you can run it from the command line. The program supports the following options: + +- **--help** + Display a help message with usage instructions and exit. + +```bash + ./screen_recorder --help +``` +- --version +Display the application version (defined in version.h) and exit. + +```bash +./screen_recorder --version +``` + +- --debug +Enable additional debug output (verbose logging of key operations). + +```bash +./screen_recorder --debug +``` + +When run without these flags, the GUI will start and you can interact with it to choose the recording source, set parameters, and start/stop recordings. + +## Internal Code Operation + +### Screen Capture: +The recorder module uses X11 to grab either the full screen or a selected window. Using XRandR, it determines monitor geometry when a specific monitor is chosen. + +### Audio Capture: +Audio is captured via ALSA, with the possibility of toggling it on or off dynamically. + +### Encoding: +Video frames (captured as RGB) are converted and encoded in H.264 via FFmpeg. Audio frames are captured in PCM (S16) and then converted and encoded. The output file is written to disk. + +### Multithreading: +Separate threads are used for capturing audio, capturing video, and (optionally) capturing webcam frames to avoid blocking operations and improve efficiency during long recordings. + +## Future Improvements +* Implement an AVFrame pooling system or ring buffer to reduce allocation overhead. + +* Investigate hardware-accelerated encoding (NVENC/QuickSync/VA-API) for improved performance. + +* Explore asynchronous file I/O to prevent disk write delays during long recordings. + +* Enhance debug logging throughout the code. diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..f9132b3 --- /dev/null +++ b/TODO.md @@ -0,0 +1,61 @@ +# TODO + +This document outlines potential improvements and optimizations for the Screen Recorder project. These items are ideas and enhancements that could be implemented to improve performance, resource management, production robustness, and debugging. + +## Performance & Resource Optimizations + +- **Thread & Buffer Management** + - [ ] Implement pooling or reuse of AVFrame objects in the encoder. + - [ ] Explore adding a ring buffer for incoming video and audio frames so encoding or file I/O does not block capture. + - [ ] Investigate asynchronous I/O for file writes to avoid disk bottlenecks during long recordings. + +- **Hardware Acceleration** + - [ ] Research and integrate support for hardware-accelerated encoding using platforms such as NVENC, Intel QuickSync, or VA-API when available. + +- **Screen Capture Enhancements** + - [ ] Explore enabling XShm (shared memory) for X11 screen capture to improve capture speed. + - [ ] Profile the capture loop and encoding pipeline to identify and optimize any bottlenecks. + +## Enhanced Debugging & Command-line Options + +- **Global Debug Logging** + - [ ] Introduce a global debug flag (e.g. `g_debug`) that, when enabled via the `--debug` command-line flag, prints detailed debug messages throughout the code. + - [ ] Add additional debug log statements in critical parts of the program (e.g., in audio capture, screen capture, encoder, and thread management) to facilitate troubleshooting. + +- **Command-line Options** + - [ ] Implement command-line parsing with options: + - `--help`: Prints a detailed usage message. + - `--version`: Displays the version (from a new `version.h` file) and exits. + - `--debug`: Enables extra debug logging. + - [ ] Improve help message and documentation output. + +## Documentation & Build System + +- **Makefile Improvements** + - [ ] Upgrade the Makefile to use wildcards for source file detection. + - [ ] Ensure the Makefile creates an output directory for object files automatically. + - [ ] Add comments and a clean target to simplify building and cleaning the project. + +- **README Documentation** + - [ ] Write a professional README.md that provides: + - An overview of how the code operates internally. + - A list of dependencies needed (GTK+3, FFmpeg libraries, ALSA, X11, XRandR, etc.). + - Build and installation instructions. + - Usage instructions for command-line options. + - Future work and potential improvements. + +## Additional Production Considerations + +- **Robust Error Handling** + - [ ] Review and enhance error checking and logging in each module. + - [ ] Consider implementing automatic cleanup or recovery mechanisms in case of errors during a long recording session. + +- **Modularization & Refactoring** + - [ ] Refactor the code for clearer separation of concerns, so that each module (audio, video, GUI, encoding) is easier to optimize individually. + - [ ] Consider extracting common routines (e.g., debug logging, resource pooling) into separate modules or libraries. + +- **Testing and Benchmarking** + - [ ] Develop test cases to measure performance during long recording sessions. + - [ ] Benchmark resource usage (CPU, memory, disk I/O) to identify further optimization opportunities. + + diff --git a/ceras b/ceras new file mode 100755 index 0000000000000000000000000000000000000000..89836c1b06c3296765ffac116f8049fcd86a83d0 GIT binary patch literal 61440 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 +#include + +typedef struct { + snd_pcm_t *pcm_handle; + int is_recording; + int sample_rate; + int channels; + int capture_audio; // New flag for dynamic audio recording toggle (1 = enabled, 0 = disabled) +} AudioContext; + +/* Initialize and configure ALSA capture */ +AudioContext* audio_init(); + +/* Start audio capture */ +int audio_start(AudioContext* ctx); + +/* Stop audio capture */ +int audio_stop(AudioContext* ctx); + +/* Cleanup audio capture resources */ +void audio_cleanup(AudioContext* ctx); + +/* Capture audio into the provided buffer. + Returns the number of frames captured. */ +int audio_capture(AudioContext* ctx, uint8_t *buffer, int buffer_size); + +/* Set dynamic audio capture toggle. + If enabled is 1, audio will be captured; if 0, audio capture is skipped. +*/ +void audio_set_capture(AudioContext* ctx, int enabled); + +#endif // AUDIO_H + diff --git a/include/config.h b/include/config.h new file mode 100644 index 0000000..1fa8a9c --- /dev/null +++ b/include/config.h @@ -0,0 +1,15 @@ +#ifndef CONFIG_H +#define CONFIG_H + +/* Define CSS colors and style values */ +#define WINDOW_BG_COLOR "#735290" +#define BUTTON_BG_COLOR "#D0C5FC" +#define BUTTON_TEXT_COLOR "#FFFFFF" +#define BUTTON_BORDER_RADIUS "5px" +#define BUTTON_PADDING "5px" +#define LABEL_TEXT_COLOR "#FFFFFF" +#define COMBO_BG_COLOR "#A28CC6" +#define COMBO_TEXT_COLOR "#FFFFFF" + +#endif /* CONFIG_H */ + diff --git a/include/debug.h b/include/debug.h new file mode 100644 index 0000000..e01f62d --- /dev/null +++ b/include/debug.h @@ -0,0 +1,16 @@ +#ifndef DEBUG_H +#define DEBUG_H + +#include + +extern int g_debug; // Global debug flag + +#define DEBUG_LOG(fmt, ...) \ + do { \ + if (g_debug) { \ + fprintf(stderr, "[DEBUG] " fmt "\n", ##__VA_ARGS__); \ + } \ + } while (0) + +#endif // DEBUG_H + diff --git a/include/encoder.h b/include/encoder.h new file mode 100644 index 0000000..9947761 --- /dev/null +++ b/include/encoder.h @@ -0,0 +1,67 @@ +#ifndef ENCODER_H +#define ENCODER_H + +#include +#include +#include +#include +#include + +// Default Audio Bitrate for AAC/Opus (lossy codecs) +#define DEFAULT_AUDIO_BIT_RATE 64000 + +/* Video quality enumeration */ +typedef enum { + QUALITY_LOW, + QUALITY_MEDIUM, + QUALITY_HIGH +} Quality; + +/* Audio codec enumeration */ +typedef enum { + AUDIO_CODEC_AAC, + AUDIO_CODEC_PCM, + AUDIO_CODEC_OPUS +} AudioCodec; + +typedef struct { + AVFormatContext *fmt_ctx; + AVCodecContext *video_enc_ctx; + AVStream *video_stream; + AVCodecContext *audio_enc_ctx; + AVStream *audio_stream; + struct SwsContext *sws_ctx; + struct SwrContext *swr_ctx; // audio resampling context + int frame_index; + int64_t audio_pts; // running PTS (in samples) for audio + Quality quality; + char filename[512]; // The output filename +} EncoderContext; + +/* + * Initializes the encoder. + * 'width' and 'height' are the recording dimensions (which may differ from native screen size). + * 'fps' is the capture framerate, and 'sample_rate' and 'channels' are audio parameters. + * 'audio_codec' selects the audio codec: AAC (lossy), PCM (lossless), or Opus (modern lossy). + * 'audio_bitrate' specifies the desired audio bitrate (e.g., DEFAULT_AUDIO_BIT_RATE for AAC/Opus). + * The output file is initially created in ~/Videos/Screenrecords/ with a generated name. + */ +EncoderContext* encoder_init(Quality quality, int width, int height, int fps, int sample_rate, int channels, AudioCodec audio_codec, int audio_bitrate); + +/* Encode one video frame (input data in RGB24 format) */ +int encoder_encode_video_frame(EncoderContext* ctx, uint8_t* data); + +/* Encode one audio frame with PCM data. + The input data is expected to be S16 interleaved. + Internally, the data is converted to the encoder’s sample format. +*/ +int encoder_encode_audio_frame(EncoderContext* ctx, uint8_t* data, int size); + +/* Finalize the output file */ +int encoder_finalize(EncoderContext* ctx); + +/* Cleanup the encoder resources */ +void encoder_cleanup(EncoderContext* ctx); + +#endif // ENCODER_H + diff --git a/include/gui.h b/include/gui.h new file mode 100644 index 0000000..84dc11e --- /dev/null +++ b/include/gui.h @@ -0,0 +1,79 @@ +#ifndef GUI_H +#define GUI_H + +#include +#include "encoder.h" /* For Quality and AudioCodec */ + +/* Available recording sources */ +typedef enum { + RECORD_SOURCE_ALL, /* record union of all monitors */ + RECORD_SOURCE_WINDOW, /* Record a specific window */ + RECORD_SOURCE_MONITOR /* Record a single monitor */ +} RecordSource; + +/* GUIComponents structure, extended with additional controls */ +typedef struct { + GtkWidget *window; + GtkWidget *record_toggle; /* Button to start/stop recording */ + GtkWidget *camera_toggle; /* Button to toggle webcam preview */ + GtkWidget *audio_toggle; /* New: Toggle button for audio recording */ + GtkWidget *source_combo; /* Combo box: "All", "Window", plus individual monitor names */ + GtkWidget *quality_combo; /* Combo box: "Low", "Medium", "High" */ + GtkWidget *resolution_combo; /* Combo box: "Full", "1080p", "720p", "480p" */ + GtkWidget *audio_codec_combo; /* New: Combo box for Audio Codec (AAC, PCM, Opus) */ + GtkWidget *fps_selector; /* New: Selector for FPS (e.g., SpinButton) */ + GtkWidget *webcam_resolution_combo; /* New: Combo for webcam resolution (Default, 640x480) */ + GtkWidget *info_label; /* Displays recording info */ + GtkWidget *preview_area; /* Webcam preview area */ +} GUIComponents; + +/* Initialize the GUI and return main components */ +GUIComponents* gui_init(); + +/* Update the info label with a string */ +void gui_update_info(GUIComponents* gui, const char* info); + +/* Update the webcam preview area with a GdkPixbuf */ +void gui_update_preview(GUIComponents* gui, GdkPixbuf* pixbuf); + +/* Free GUI resources */ +void gui_cleanup(GUIComponents* gui); + +/* Get the currently selected recording source. + Returns: + - RECORD_SOURCE_WINDOW if the selected entry equals "Window" + - RECORD_SOURCE_ALL if the selected entry equals "All" + - RECORD_SOURCE_MONITOR otherwise (i.e. if it matches a monitor name) +*/ +RecordSource gui_get_record_source(GUIComponents* gui); + +/* Get the quality setting (Low/Medium/High) */ +Quality gui_get_quality(GUIComponents* gui); + +/* Get the resolution selection string (e.g., "Full", "1080p", etc.) */ +const char* gui_get_resolution(GUIComponents* gui); + +/* Get the selected monitor name from the source combo. + Returns NULL if the user selected "All" or "Window". +*/ +const char* gui_get_monitor_name(GUIComponents* gui); + +/* Populate the source combo with available monitors in addition to "All" and "Window". */ +void gui_populate_source_combo(GUIComponents *gui); + +/* Get the selected audio codec from the GUI. + Returns one of AUDIO_CODEC_AAC, AUDIO_CODEC_PCM, AUDIO_CODEC_OPUS. +*/ +AudioCodec gui_get_audio_codec(GUIComponents* gui); + +/* Get the selected FPS value */ +int gui_get_fps(GUIComponents* gui); + +/* Get the selected webcam resolution option (e.g., "Default" or "640x480") */ +const char* gui_get_webcam_resolution(GUIComponents* gui); + +int get_monitor_geometry(const char *monitor_name, int *x, int *y, int *width, int *height); + + +#endif // GUI_H + diff --git a/include/recorder.h b/include/recorder.h new file mode 100644 index 0000000..164ce3f --- /dev/null +++ b/include/recorder.h @@ -0,0 +1,57 @@ +#ifndef RECORDER_H +#define RECORDER_H + +#include +#include +#include + +/* Recorder context now supports both full-screen and window-based capture */ +typedef struct { + Display *display; + Window root; + Window target; // Target window for capture (if any) + int screen; + int x, y; // Capture region origin + int width; + int height; + int is_capturing; + int is_window_capture; // Flag: if 1, capture only the target window + int use_shm; // Flag: 1 if XShm is used + XShmSegmentInfo shm_info; // For XShm +} RecorderContext; + +/* + * Initializes recorder. If target is 0, captures the full screen; + * otherwise, uses the specified target window’s geometry. + */ +RecorderContext* recorder_init(Window target); + +/* + * Grabs the pointer and lets the user click on a window to capture. + * This function blocks until a window is selected. + * On return, *target, *x, *y, *width, *height are filled out. + */ +void recorder_select_window(Display *display, Window *target, int *x, int *y, int *width, int *height); + +/* Begin capturing (sets a flag) */ +int recorder_start(RecorderContext* ctx); + +/* Stop capturing */ +int recorder_stop(RecorderContext* ctx); + +/* Free allocated recorder context */ +void recorder_cleanup(RecorderContext* ctx); + +/* Capture one frame from the screen or target window. + Returns a buffer in RGB24 format (caller must free). + 'linesize' returns the number of bytes per row. +*/ +uint8_t* recorder_capture_frame(RecorderContext* ctx, int *linesize); + +/* Update window geometry dynamically for window capture. + Re-fetches attributes of the target window. +*/ +int recorder_update_window_geometry(RecorderContext *ctx); + +#endif // RECORDER_H + diff --git a/include/version.h b/include/version.h new file mode 100644 index 0000000..2f185e9 --- /dev/null +++ b/include/version.h @@ -0,0 +1,7 @@ +#ifndef VERSION_H +#define VERSION_H + +#define APP_VERSION "0.0.1" + +#endif // VERSION_H + diff --git a/src/audio.c b/src/audio.c new file mode 100644 index 0000000..591a20c --- /dev/null +++ b/src/audio.c @@ -0,0 +1,69 @@ +/* src/audio.c */ +#include "audio.h" +#include +#include +#include +#include + +AudioContext* audio_init() { + AudioContext* ctx = malloc(sizeof(AudioContext)); + if(!ctx) return NULL; + int err = snd_pcm_open(&ctx->pcm_handle, "default", SND_PCM_STREAM_CAPTURE, 0); + if(err < 0) { + fprintf(stderr, "Unable to open PCM device: %s\n", snd_strerror(err)); + free(ctx); + return NULL; + } + ctx->sample_rate = 44100; + ctx->channels = 2; + err = snd_pcm_set_params(ctx->pcm_handle, SND_PCM_FORMAT_S16_LE, + SND_PCM_ACCESS_RW_INTERLEAVED, ctx->channels, + ctx->sample_rate, 1, 500000); + if(err < 0) { + fprintf(stderr, "Unable to set PCM parameters: %s\n", snd_strerror(err)); + snd_pcm_close(ctx->pcm_handle); + free(ctx); + return NULL; + } + ctx->is_recording = 0; + ctx->capture_audio = 1; // Audio capturing enabled by default + return ctx; +} + +int audio_start(AudioContext* ctx) { + if(!ctx) return -1; + ctx->is_recording = 1; + return 0; +} + +int audio_stop(AudioContext* ctx) { + if(!ctx) return -1; + ctx->is_recording = 0; + return 0; +} + +void audio_cleanup(AudioContext* ctx) { + if(ctx) { + if(ctx->pcm_handle) + snd_pcm_close(ctx->pcm_handle); + free(ctx); + } +} + +int audio_capture(AudioContext* ctx, uint8_t *buffer, int buffer_size) { + if(!ctx || !ctx->is_recording) + return -1; + if (!ctx->capture_audio) // Skip audio capture if disabled via toggle + return 0; + int frames = snd_pcm_readi(ctx->pcm_handle, buffer, buffer_size / (ctx->channels * 2)); + if (frames < 0) { + frames = snd_pcm_recover(ctx->pcm_handle, frames, 0); + } + return frames; +} + +void audio_set_capture(AudioContext* ctx, int enabled) { + if(ctx) + ctx->capture_audio = enabled; +} + diff --git a/src/encoder.c b/src/encoder.c new file mode 100644 index 0000000..94cf6fb --- /dev/null +++ b/src/encoder.c @@ -0,0 +1,358 @@ +/* src/encoder.c */ +#include "encoder.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define VIDEO_BIT_RATE 400000 +// DEFAULT_AUDIO_BIT_RATE is defined in encoder.h + +// Helper: Generate a filename based on current time. +static void generate_filename(char* buffer, size_t size) { + time_t t = time(NULL); + struct tm *tm_info = localtime(&t); + strftime(buffer, size, "screenrecording_%Y%m%d_%H%M%S.mp4", tm_info); +} + +static int setup_video_stream(EncoderContext* ctx, int width, int height, int fps) { + const AVCodec *codec = avcodec_find_encoder(AV_CODEC_ID_H264); + if (!codec) { + fprintf(stderr, "H.264 codec not found\n"); + return -1; + } + ctx->video_stream = avformat_new_stream(ctx->fmt_ctx, codec); + if (!ctx->video_stream) { + fprintf(stderr, "Could not allocate video stream\n"); + return -1; + } + ctx->video_enc_ctx = avcodec_alloc_context3(codec); + if (!ctx->video_enc_ctx) { + fprintf(stderr, "Could not allocate video codec context\n"); + return -1; + } + ctx->video_enc_ctx->codec_id = AV_CODEC_ID_H264; + ctx->video_enc_ctx->bit_rate = VIDEO_BIT_RATE; + ctx->video_enc_ctx->width = width; + ctx->video_enc_ctx->height = height; + ctx->video_enc_ctx->time_base = (AVRational){1, fps}; + ctx->video_enc_ctx->framerate = (AVRational){fps, 1}; + ctx->video_enc_ctx->gop_size = 12; + ctx->video_enc_ctx->max_b_frames = 2; + ctx->video_enc_ctx->pix_fmt = AV_PIX_FMT_YUV420P; + if (ctx->fmt_ctx->oformat->flags & AVFMT_GLOBALHEADER) + ctx->video_enc_ctx->flags |= AV_CODEC_FLAG_GLOBAL_HEADER; + int ret = avcodec_open2(ctx->video_enc_ctx, codec, NULL); + if (ret < 0) { + char errbuf[128]; + av_strerror(ret, errbuf, sizeof(errbuf)); + fprintf(stderr, "Could not open video codec: %s\n", errbuf); + return ret; + } + ret = avcodec_parameters_from_context(ctx->video_stream->codecpar, ctx->video_enc_ctx); + if (ret < 0) { + fprintf(stderr, "Could not copy video codec parameters\n"); + return ret; + } + ctx->video_stream->time_base = ctx->video_enc_ctx->time_base; + ctx->sws_ctx = sws_getContext(width, height, AV_PIX_FMT_RGB24, + width, height, AV_PIX_FMT_YUV420P, + SWS_BICUBIC, NULL, NULL, NULL); + if (!ctx->sws_ctx) { + fprintf(stderr, "Could not initialize the scaling context\n"); + return -1; + } + return 0; +} + +static int setup_audio_stream(EncoderContext* ctx, int sample_rate, int channels, AudioCodec audio_codec, int audio_bitrate) { + const AVCodec *codec = NULL; + switch(audio_codec) { + case AUDIO_CODEC_AAC: + codec = avcodec_find_encoder(AV_CODEC_ID_AAC); + break; + case AUDIO_CODEC_PCM: + codec = avcodec_find_encoder(AV_CODEC_ID_PCM_S16LE); + break; + case AUDIO_CODEC_OPUS: + codec = avcodec_find_encoder(AV_CODEC_ID_OPUS); + break; + default: + codec = avcodec_find_encoder(AV_CODEC_ID_AAC); + break; + } + if (!codec) { + fprintf(stderr, "Audio codec not found for selected option\n"); + return -1; + } + ctx->audio_stream = avformat_new_stream(ctx->fmt_ctx, codec); + if (!ctx->audio_stream) { + fprintf(stderr, "Could not allocate audio stream\n"); + return -1; + } + ctx->audio_enc_ctx = avcodec_alloc_context3(codec); + if (!ctx->audio_enc_ctx) { + fprintf(stderr, "Could not allocate audio codec context\n"); + return -1; + } + if(audio_codec == AUDIO_CODEC_PCM) { + /* For PCM, bitrate is not used. */ + ctx->audio_enc_ctx->bit_rate = 0; + } else { + ctx->audio_enc_ctx->bit_rate = audio_bitrate; + } + if(audio_codec == AUDIO_CODEC_OPUS && sample_rate != 48000) { + fprintf(stderr, "Opus codec requires a sample rate of 48000 Hz, forcing sample_rate to 48000\n"); + sample_rate = 48000; + } + ctx->audio_enc_ctx->sample_fmt = codec->sample_fmts ? codec->sample_fmts[0] : AV_SAMPLE_FMT_FLTP; + ctx->audio_enc_ctx->sample_rate = sample_rate; + av_channel_layout_default(&ctx->audio_enc_ctx->ch_layout, channels); + ctx->audio_enc_ctx->time_base = (AVRational){1, sample_rate}; + if (ctx->fmt_ctx->oformat->flags & AVFMT_GLOBALHEADER) + ctx->audio_enc_ctx->flags |= AV_CODEC_FLAG_GLOBAL_HEADER; + int ret = avcodec_open2(ctx->audio_enc_ctx, codec, NULL); + if (ret < 0) { + fprintf(stderr, "Could not open audio codec\n"); + return ret; + } + ret = avcodec_parameters_from_context(ctx->audio_stream->codecpar, ctx->audio_enc_ctx); + if (ret < 0) { + fprintf(stderr, "Could not copy audio codec parameters\n"); + return ret; + } + ctx->audio_stream->time_base = ctx->audio_enc_ctx->time_base; + + /* Initialize resampling context */ + ctx->swr_ctx = swr_alloc(); + if (!ctx->swr_ctx) { + fprintf(stderr, "Could not allocate resampling context\n"); + return -1; + } + av_opt_set_int(ctx->swr_ctx, "in_channel_layout", channels == 2 ? AV_CH_LAYOUT_STEREO : AV_CH_LAYOUT_MONO, 0); + av_opt_set_int(ctx->swr_ctx, "out_channel_layout", ctx->audio_enc_ctx->ch_layout.nb_channels == 2 ? AV_CH_LAYOUT_STEREO : AV_CH_LAYOUT_MONO, 0); + av_opt_set_int(ctx->swr_ctx, "in_sample_rate", sample_rate, 0); + av_opt_set_int(ctx->swr_ctx, "out_sample_rate", sample_rate, 0); + av_opt_set_sample_fmt(ctx->swr_ctx, "in_sample_fmt", AV_SAMPLE_FMT_S16, 0); + av_opt_set_sample_fmt(ctx->swr_ctx, "out_sample_fmt", ctx->audio_enc_ctx->sample_fmt, 0); + ret = swr_init(ctx->swr_ctx); + if (ret < 0) { + fprintf(stderr, "Failed to initialize the resampling context\n"); + return ret; + } + return 0; +} + +EncoderContext* encoder_init(Quality quality, int width, int height, int fps, int sample_rate, int channels, AudioCodec audio_codec, int audio_bitrate) { + EncoderContext* ctx = malloc(sizeof(EncoderContext)); + if (!ctx) return NULL; + memset(ctx, 0, sizeof(EncoderContext)); + ctx->quality = quality; + ctx->frame_index = 0; + ctx->audio_pts = 0; // initialize audio pts + + char filepath[1024]; + const char *home = getenv("HOME"); + if (!home) home = "."; + snprintf(filepath, sizeof(filepath), "%s/Videos/Screenrecords/", home); + char mkdir_cmd[1200]; + snprintf(mkdir_cmd, sizeof(mkdir_cmd), "mkdir -p %s", filepath); + system(mkdir_cmd); + generate_filename(ctx->filename, sizeof(ctx->filename)); + /* If PCM is selected, change extension from .mp4 to .mov */ + if (audio_codec == AUDIO_CODEC_PCM) { + char *dot = strrchr(ctx->filename, '.'); + if (dot) + strcpy(dot, ".mov"); + } + char fullpath[2048]; + snprintf(fullpath, sizeof(fullpath), "%s%s", filepath, ctx->filename); + + /* For PCM, force MOV container; otherwise use default */ + int ret; + if (audio_codec == AUDIO_CODEC_PCM) + ret = avformat_alloc_output_context2(&ctx->fmt_ctx, NULL, "mov", fullpath); + else + ret = avformat_alloc_output_context2(&ctx->fmt_ctx, NULL, NULL, fullpath); + if (ret < 0 || !ctx->fmt_ctx) { + fprintf(stderr, "Could not create output context\n"); + free(ctx); + return NULL; + } + + ret = setup_video_stream(ctx, width, height, fps); + if (ret < 0) { + free(ctx); + return NULL; + } + ret = setup_audio_stream(ctx, sample_rate, channels, audio_codec, audio_bitrate); + if (ret < 0) { + free(ctx); + return NULL; + } + if (!(ctx->fmt_ctx->oformat->flags & AVFMT_NOFILE)) { + ret = avio_open(&ctx->fmt_ctx->pb, fullpath, AVIO_FLAG_WRITE); + if (ret < 0) { + fprintf(stderr, "Could not open output file '%s'\n", fullpath); + free(ctx); + return NULL; + } + } + ret = avformat_write_header(ctx->fmt_ctx, NULL); + if (ret < 0) { + fprintf(stderr, "Error occurred when opening output file\n"); + free(ctx); + return NULL; + } + printf("Encoder initialized, output file: %s\n", fullpath); + return ctx; +} + +int encoder_encode_video_frame(EncoderContext* ctx, uint8_t* data) { + if (!ctx || !data) return -1; + int ret; + AVFrame *frame = av_frame_alloc(); + if (!frame) return -1; + frame->format = AV_PIX_FMT_YUV420P; + frame->width = ctx->video_enc_ctx->width; + frame->height = ctx->video_enc_ctx->height; + ret = av_frame_get_buffer(frame, 32); + if (ret < 0) { + fprintf(stderr, "Could not allocate frame data\n"); + av_frame_free(&frame); + return ret; + } + AVFrame *rgb_frame = av_frame_alloc(); + if (!rgb_frame) { + av_frame_free(&frame); + return -1; + } + rgb_frame->format = AV_PIX_FMT_RGB24; + rgb_frame->width = ctx->video_enc_ctx->width; + rgb_frame->height = ctx->video_enc_ctx->height; + ret = av_image_fill_arrays(rgb_frame->data, rgb_frame->linesize, data, + AV_PIX_FMT_RGB24, ctx->video_enc_ctx->width, ctx->video_enc_ctx->height, 1); + if (ret < 0) { + fprintf(stderr, "Could not fill RGB frame\n"); + av_frame_free(&frame); + av_frame_free(&rgb_frame); + return ret; + } + sws_scale(ctx->sws_ctx, (const uint8_t* const*)rgb_frame->data, rgb_frame->linesize, 0, + ctx->video_enc_ctx->height, frame->data, frame->linesize); + frame->pts = ctx->frame_index++; + ret = avcodec_send_frame(ctx->video_enc_ctx, frame); + if (ret < 0) { + fprintf(stderr, "Error sending video frame\n"); + av_frame_free(&frame); + av_frame_free(&rgb_frame); + return ret; + } + AVPacket *pkt = av_packet_alloc(); + ret = avcodec_receive_packet(ctx->video_enc_ctx, pkt); + if (ret == 0) { + pkt->stream_index = ctx->video_stream->index; + pkt->pts = av_rescale_q(pkt->pts, ctx->video_enc_ctx->time_base, ctx->video_stream->time_base); + pkt->dts = av_rescale_q(pkt->dts, ctx->video_enc_ctx->time_base, ctx->video_stream->time_base); + pkt->duration = av_rescale_q(pkt->duration, ctx->video_enc_ctx->time_base, ctx->video_stream->time_base); + ret = av_interleaved_write_frame(ctx->fmt_ctx, pkt); + av_packet_free(&pkt); + } else { + av_packet_free(&pkt); + } + av_frame_free(&frame); + av_frame_free(&rgb_frame); + return ret; +} + +int encoder_encode_audio_frame(EncoderContext* ctx, uint8_t* data, int size) { + if (!ctx || !data) return -1; + int ret; + AVFrame *frame = av_frame_alloc(); + if (!frame) return -1; + + /* Determine number of input samples based on S16 input format */ + int in_samples = size / (ctx->audio_enc_ctx->ch_layout.nb_channels * sizeof(int16_t)); + + /* For PCM we bypass the fixed frame size and use the available samples */ + if(ctx->audio_enc_ctx->codec_id == AV_CODEC_ID_PCM_S16LE) { + frame->nb_samples = in_samples; + } else { + frame->nb_samples = ctx->audio_enc_ctx->frame_size; + } + + frame->format = ctx->audio_enc_ctx->sample_fmt; + ret = av_channel_layout_copy(&frame->ch_layout, &ctx->audio_enc_ctx->ch_layout); + if (ret < 0) { + fprintf(stderr, "Could not copy channel layout\n"); + av_frame_free(&frame); + return ret; + } + ret = av_frame_get_buffer(frame, 0); + if (ret < 0) { + av_frame_free(&frame); + return ret; + } + + /* Use swr_convert to convert input S16 to encoder sample format */ + int converted = swr_convert(ctx->swr_ctx, frame->data, frame->nb_samples, (const uint8_t **)&data, in_samples); + if (converted < 0) { + fprintf(stderr, "Error while converting audio samples\n"); + av_frame_free(&frame); + return converted; + } + frame->nb_samples = converted; + frame->pts = ctx->audio_pts; + ctx->audio_pts += converted; + + ret = avcodec_send_frame(ctx->audio_enc_ctx, frame); + if (ret < 0) { + av_frame_free(&frame); + return ret; + } + AVPacket *pkt = av_packet_alloc(); + ret = avcodec_receive_packet(ctx->audio_enc_ctx, pkt); + if (ret == 0) { + pkt->stream_index = ctx->audio_stream->index; + pkt->pts = av_rescale_q(pkt->pts, ctx->audio_enc_ctx->time_base, ctx->audio_stream->time_base); + pkt->dts = av_rescale_q(pkt->dts, ctx->audio_enc_ctx->time_base, ctx->audio_stream->time_base); + pkt->duration = av_rescale_q(pkt->duration, ctx->audio_enc_ctx->time_base, ctx->audio_stream->time_base); + ret = av_interleaved_write_frame(ctx->fmt_ctx, pkt); + av_packet_free(&pkt); + } else { + av_packet_free(&pkt); + } + av_frame_free(&frame); + return ret; +} + +int encoder_finalize(EncoderContext* ctx) { + if (!ctx) return -1; + int ret = av_write_trailer(ctx->fmt_ctx); + if (ret < 0) + fprintf(stderr, "Error writing trailer\n"); + return ret; +} + +void encoder_cleanup(EncoderContext* ctx) { + if (!ctx) return; + if (ctx->swr_ctx) { + swr_free(&ctx->swr_ctx); + } + if (ctx->sws_ctx) sws_freeContext(ctx->sws_ctx); + if (ctx->video_enc_ctx) avcodec_free_context(&ctx->video_enc_ctx); + if (ctx->audio_enc_ctx) avcodec_free_context(&ctx->audio_enc_ctx); + if (ctx->fmt_ctx) { + if (!(ctx->fmt_ctx->oformat->flags & AVFMT_NOFILE)) + avio_closep(&ctx->fmt_ctx->pb); + avformat_free_context(ctx->fmt_ctx); + } + free(ctx); +} + diff --git a/src/gui.c b/src/gui.c new file mode 100644 index 0000000..40586d5 --- /dev/null +++ b/src/gui.c @@ -0,0 +1,285 @@ +/* gui.c */ +#include "gui.h" +#include "config.h" +#include +#include +#include +#include +#include + +/* Define default position offsets relative to target monitor */ +#define DEFAULT_OFFSET_X 100 +#define DEFAULT_OFFSET_Y 100 + +struct _GUIComponents { + GtkWidget *window; + GtkWidget *record_toggle; + GtkWidget *camera_toggle; + GtkWidget *audio_toggle; /* Audio toggle button */ + GtkWidget *source_combo; + GtkWidget *quality_combo; + GtkWidget *resolution_combo; + GtkWidget *audio_codec_combo; /* Audio codec selection */ + GtkWidget *fps_selector; /* FPS selector */ + GtkWidget *webcam_resolution_combo; /* Webcam resolution selection */ + GtkWidget *info_label; + GtkWidget *preview_area; +}; + +/* Generate CSS using values from config.h, and insert newlines between rules */ +static char *generate_css(void) { + char *css_data = malloc(512); + if (!css_data) + return NULL; + snprintf(css_data, 512, + "window { background-color: %s; }\n" + "button { background-color: %s; color: %s; border-radius: %s; padding: %s; }\n" + "label { color: %s; }\n" + "comboboxtext, spinbutton { background-color: %s; color: %s; }", + WINDOW_BG_COLOR, + BUTTON_BG_COLOR, BUTTON_TEXT_COLOR, BUTTON_BORDER_RADIUS, BUTTON_PADDING, + LABEL_TEXT_COLOR, + COMBO_BG_COLOR, COMBO_TEXT_COLOR); + return css_data; +} + +GUIComponents* gui_init() { + GUIComponents* gui = malloc(sizeof(GUIComponents)); + if (!gui) + return NULL; + + gui->window = gtk_window_new(GTK_WINDOW_TOPLEVEL); + gtk_window_set_title(GTK_WINDOW(gui->window), "CtheScreen"); + gtk_window_set_default_size(GTK_WINDOW(gui->window), 800, 600); + gtk_window_set_position(GTK_WINDOW(gui->window), GTK_WIN_POS_CENTER); + gtk_window_set_resizable(GTK_WINDOW(gui->window), TRUE); + /* Set the type hint to DIALOG so that DWM treats the window as floating but still manages it */ + gtk_window_set_type_hint(GTK_WINDOW(gui->window), GDK_WINDOW_TYPE_HINT_DIALOG); + gtk_window_set_keep_above(GTK_WINDOW(gui->window), TRUE); + gtk_window_set_skip_taskbar_hint(GTK_WINDOW(gui->window), TRUE); + + /* Apply CSS styling using dynamically generated CSS from config.h */ + char *css_data = generate_css(); + GtkCssProvider *provider = gtk_css_provider_new(); + gtk_css_provider_load_from_data(provider, css_data, -1, NULL); + GtkStyleContext *context = gtk_widget_get_style_context(gui->window); + gtk_style_context_add_provider(context, GTK_STYLE_PROVIDER(provider), GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); + g_object_unref(provider); + free(css_data); + + GtkWidget *grid = gtk_grid_new(); + gtk_grid_set_row_spacing(GTK_GRID(grid), 10); + gtk_grid_set_column_spacing(GTK_GRID(grid), 10); + gtk_container_set_border_width(GTK_CONTAINER(grid), 10); + gtk_container_add(GTK_CONTAINER(gui->window), grid); + + /* Row 0: Recording toggle, Camera toggle, Audio toggle */ + gui->record_toggle = gtk_toggle_button_new_with_label("Start Recording"); + gtk_widget_set_hexpand(gui->record_toggle, TRUE); + gtk_grid_attach(GTK_GRID(grid), gui->record_toggle, 0, 0, 1, 1); + + gui->camera_toggle = gtk_toggle_button_new_with_label("Camera On"); + gtk_widget_set_hexpand(gui->camera_toggle, TRUE); + gtk_grid_attach(GTK_GRID(grid), gui->camera_toggle, 1, 0, 1, 1); + + gui->audio_toggle = gtk_toggle_button_new_with_label("Audio On"); + gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(gui->audio_toggle), TRUE); + gtk_widget_set_hexpand(gui->audio_toggle, TRUE); + gtk_grid_attach(GTK_GRID(grid), gui->audio_toggle, 2, 0, 1, 1); + + /* Row 1: Source selection and Quality selection */ + GtkWidget *source_label = gtk_label_new("Capture Source:"); + gtk_grid_attach(GTK_GRID(grid), source_label, 0, 1, 1, 1); + gui->source_combo = gtk_combo_box_text_new(); + gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(gui->source_combo), "All"); + gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(gui->source_combo), "Window"); + gui_populate_source_combo(gui); + gtk_grid_attach(GTK_GRID(grid), gui->source_combo, 1, 1, 1, 1); + + GtkWidget *quality_label = gtk_label_new("Encoding Quality:"); + gtk_widget_set_tooltip_text(quality_label, "Controls video encoding quality (bitrate, etc.)"); + gtk_grid_attach(GTK_GRID(grid), quality_label, 2, 1, 1, 1); + gui->quality_combo = gtk_combo_box_text_new(); + gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(gui->quality_combo), "Low"); + gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(gui->quality_combo), "Medium"); + gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(gui->quality_combo), "High"); + gtk_combo_box_set_active(GTK_COMBO_BOX(gui->quality_combo), 1); + gtk_grid_attach(GTK_GRID(grid), gui->quality_combo, 3, 1, 1, 1); + + /* Row 2: Resolution selection and FPS selector */ + GtkWidget *resolution_label = gtk_label_new("Capture Resolution:"); + gtk_widget_set_tooltip_text(resolution_label, "Sets the output dimensions for recording"); + gtk_grid_attach(GTK_GRID(grid), resolution_label, 0, 2, 1, 1); + gui->resolution_combo = gtk_combo_box_text_new(); + gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(gui->resolution_combo), "Full"); + gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(gui->resolution_combo), "1080p"); + gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(gui->resolution_combo), "720p"); + gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(gui->resolution_combo), "480p"); + gtk_combo_box_set_active(GTK_COMBO_BOX(gui->resolution_combo), 0); + gtk_grid_attach(GTK_GRID(grid), gui->resolution_combo, 1, 2, 1, 1); + + GtkWidget *fps_label = gtk_label_new("FPS:"); + gtk_grid_attach(GTK_GRID(grid), fps_label, 2, 2, 1, 1); + gui->fps_selector = gtk_spin_button_new_with_range(15, 60, 1); + gtk_spin_button_set_value(GTK_SPIN_BUTTON(gui->fps_selector), 30); + gtk_grid_attach(GTK_GRID(grid), gui->fps_selector, 3, 2, 1, 1); + + /* Row 3: Audio Codec selection and Webcam resolution selection */ + GtkWidget *audio_codec_label = gtk_label_new("Audio Codec:"); + gtk_grid_attach(GTK_GRID(grid), audio_codec_label, 0, 3, 1, 1); + gui->audio_codec_combo = gtk_combo_box_text_new(); + gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(gui->audio_codec_combo), "AAC"); + gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(gui->audio_codec_combo), "PCM"); + gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(gui->audio_codec_combo), "Opus"); + gtk_combo_box_set_active(GTK_COMBO_BOX(gui->audio_codec_combo), 0); + gtk_grid_attach(GTK_GRID(grid), gui->audio_codec_combo, 1, 3, 1, 1); + + GtkWidget *webcam_res_label = gtk_label_new("Webcam Resolution:"); + gtk_grid_attach(GTK_GRID(grid), webcam_res_label, 2, 3, 1, 1); + gui->webcam_resolution_combo = gtk_combo_box_text_new(); + gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(gui->webcam_resolution_combo), "Default"); + gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(gui->webcam_resolution_combo), "640x480"); + gtk_combo_box_set_active(GTK_COMBO_BOX(gui->webcam_resolution_combo), 0); + gtk_grid_attach(GTK_GRID(grid), gui->webcam_resolution_combo, 3, 3, 1, 1); + + /* Row 4: Info label */ + gui->info_label = gtk_label_new("Video Info: (Elapsed Time, File Size, etc.)"); + gtk_grid_attach(GTK_GRID(grid), gui->info_label, 0, 4, 4, 1); + + /* Row 5: Webcam preview area */ + gui->preview_area = gtk_image_new(); + gtk_widget_set_hexpand(gui->preview_area, TRUE); + gtk_widget_set_vexpand(gui->preview_area, TRUE); + /* Remove any fixed size so that the preview area can shrink freely */ + gtk_grid_attach(GTK_GRID(grid), gui->preview_area, 0, 5, 4, 1); + + gtk_widget_show_all(gui->window); + + /* Attempt to position window on the "eDP-1" monitor if available; + otherwise, use the primary monitor */ + { + int x, y, w, h; + if (get_monitor_geometry("eDP-1", &x, &y, &w, &h) == 0) { + gtk_window_move(GTK_WINDOW(gui->window), x + DEFAULT_OFFSET_X, y + DEFAULT_OFFSET_Y); + } else { + GdkDisplay *display = gdk_display_get_default(); + GdkMonitor *primary = gdk_display_get_primary_monitor(display); + if (primary) { + GdkRectangle geom; + gdk_monitor_get_geometry(primary, &geom); + gtk_window_move(GTK_WINDOW(gui->window), geom.x + DEFAULT_OFFSET_X, geom.y + DEFAULT_OFFSET_Y); + } else { + gtk_window_move(GTK_WINDOW(gui->window), DEFAULT_OFFSET_X, DEFAULT_OFFSET_Y); + } + } + } + return gui; +} + +void gui_update_info(GUIComponents* gui, const char* info) { + if (gui && gui->info_label) + gtk_label_set_text(GTK_LABEL(gui->info_label), info); +} + +void gui_update_preview(GUIComponents* gui, GdkPixbuf* pixbuf) { + if (gui && gui->preview_area) + gtk_image_set_from_pixbuf(GTK_IMAGE(gui->preview_area), pixbuf); +} + +/* Updated cleanup to only destroy the widget if still valid */ +void gui_cleanup(GUIComponents* gui) { + if (gui) { + if (gui->window && GTK_IS_WIDGET(gui->window)) + gtk_widget_destroy(gui->window); + free(gui); + } +} + +RecordSource gui_get_record_source(GUIComponents* gui) { + if (!gui || !gui->source_combo) + return RECORD_SOURCE_ALL; + const char* sel = gtk_combo_box_text_get_active_text(GTK_COMBO_BOX_TEXT(gui->source_combo)); + if (!sel) + return RECORD_SOURCE_ALL; + if (strcmp(sel, "Window") == 0) + return RECORD_SOURCE_WINDOW; + if (strcmp(sel, "All") == 0) + return RECORD_SOURCE_ALL; + return RECORD_SOURCE_MONITOR; +} + +Quality gui_get_quality(GUIComponents* gui) { + if (!gui || !gui->quality_combo) + return QUALITY_MEDIUM; + int active = gtk_combo_box_get_active(GTK_COMBO_BOX(gui->quality_combo)); + switch(active) { + case 0: return QUALITY_LOW; + case 1: return QUALITY_MEDIUM; + case 2: return QUALITY_HIGH; + default: return QUALITY_MEDIUM; + } +} + +const char* gui_get_resolution(GUIComponents* gui) { + if (!gui || !gui->resolution_combo) + return "Full"; + return gtk_combo_box_text_get_active_text(GTK_COMBO_BOX_TEXT(gui->resolution_combo)); +} + +const char* gui_get_monitor_name(GUIComponents* gui) { + if (!gui || !gui->source_combo) + return NULL; + const char* sel = gtk_combo_box_text_get_active_text(GTK_COMBO_BOX_TEXT(gui->source_combo)); + if (!sel) + return NULL; + if ((strcmp(sel, "All") == 0) || (strcmp(sel, "Window") == 0)) + return NULL; + return sel; +} + +void gui_populate_source_combo(GUIComponents *gui) { + Display *dpy = XOpenDisplay(NULL); + if (!dpy) return; + Window root = DefaultRootWindow(dpy); + XRRScreenResources *res = XRRGetScreenResources(dpy, root); + if (!res) { + XCloseDisplay(dpy); + return; + } + for (int i = 0; i < res->noutput; i++) { + XRROutputInfo *info = XRRGetOutputInfo(dpy, res, res->outputs[i]); + if (info && info->connection == RR_Connected && info->crtc) { + gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(gui->source_combo), info->name); + } + if (info) + XRRFreeOutputInfo(info); + } + XRRFreeScreenResources(res); + XCloseDisplay(dpy); +} + +AudioCodec gui_get_audio_codec(GUIComponents* gui) { + if (!gui || !gui->audio_codec_combo) + return AUDIO_CODEC_AAC; + const char *sel = gtk_combo_box_text_get_active_text(GTK_COMBO_BOX_TEXT(gui->audio_codec_combo)); + if (!sel) + return AUDIO_CODEC_AAC; + if (strcmp(sel, "PCM") == 0) + return AUDIO_CODEC_PCM; + if (strcmp(sel, "Opus") == 0) + return AUDIO_CODEC_OPUS; + return AUDIO_CODEC_AAC; +} + +int gui_get_fps(GUIComponents* gui) { + if (!gui || !gui->fps_selector) + return 30; + return (int) gtk_spin_button_get_value(GTK_SPIN_BUTTON(gui->fps_selector)); +} + +const char* gui_get_webcam_resolution(GUIComponents* gui) { + if (!gui || !gui->webcam_resolution_combo) + return "Default"; + return gtk_combo_box_text_get_active_text(GTK_COMBO_BOX_TEXT(gui->webcam_resolution_combo)); +} + diff --git a/src/main.c b/src/main.c new file mode 100644 index 0000000..a742ef5 --- /dev/null +++ b/src/main.c @@ -0,0 +1,518 @@ +/* src/main.c */ +#include +#include +#include +#include +#include +#include +#include +#include +#include "recorder.h" +#include "audio.h" +#include "encoder.h" +#include "gui.h" +#include "version.h" /* Must define APP_VERSION, e.g. "1.0.0" */ +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* Global debug flag: if set, extra debug info is printed */ +static int g_debug = 0; + +/* Macro for debug printing */ +#define DEBUG_PRINT(fmt, ...) \ + do { if (g_debug) fprintf(stderr, "[DEBUG] " fmt "\n", ##__VA_ARGS__); } while (0) + +/* Structure and idle callback for updating the preview safely */ +typedef struct { + GtkWidget *image; + GdkPixbuf *pixbuf; +} PreviewData; + +static gboolean update_preview_idle(gpointer data) { + PreviewData *pd = (PreviewData *) data; + if (GTK_IS_WIDGET(pd->image)) { + GtkAllocation alloc; + gtk_widget_get_allocation(pd->image, &alloc); + if (alloc.width <= 0 || alloc.height <= 0) + gtk_image_clear(GTK_IMAGE(pd->image)); + else { + GdkPixbuf *scaled = gdk_pixbuf_scale_simple(pd->pixbuf, alloc.width, alloc.height, GDK_INTERP_BILINEAR); + gtk_image_set_from_pixbuf(GTK_IMAGE(pd->image), scaled); + g_object_unref(scaled); + } + } + g_object_unref(pd->pixbuf); + free(pd); + return FALSE; +} + +/* Prototype for prompt_for_filename */ +static char* prompt_for_filename(GtkWindow *parent, const char *default_name); + +/* Global variables */ +static GUIComponents *gui; +static int is_recording = 0; +static EncoderContext* enc_ctx = NULL; +static RecorderContext* rec_ctx = NULL; +static AudioContext* audio_ctx = NULL; +static pthread_t record_thread; +static pthread_t audio_thread; +static pthread_t webcam_thread; +static time_t recording_start_time = 0; +static volatile int camera_running = 0; +static volatile int camera_thread_running = 0; + +/* Get file size (in bytes) of the output file */ +static off_t get_file_size(const char* filename) { + struct stat st; + return (stat(filename, &st) == 0) ? st.st_size : -1; +} + +/* Timer callback to update elapsed time and file size in the info label */ +static gboolean update_info_callback(gpointer data) { + if (!is_recording || !enc_ctx) return FALSE; + time_t now = time(NULL); + int elapsed = (int)difftime(now, recording_start_time); + off_t fsize = get_file_size(enc_ctx->filename); + char info[512]; + snprintf(info, sizeof(info), "Elapsed: %d sec | File Size: %ld bytes | Output: %.100s", + elapsed, (long)(fsize > 0 ? fsize : 0), enc_ctx->filename); + gui_update_info(gui, info); + return TRUE; +} + +/* Audio capture thread */ +void* audio_thread_func(void* arg) { + int buffer_frames = 1024; + int bytes_per_frame = audio_ctx->channels * 2; // S16_LE + int buffer_size = buffer_frames * bytes_per_frame; + uint8_t *buffer = malloc(buffer_size); + if (!buffer) return NULL; + while (is_recording) { + int frames = audio_capture(audio_ctx, buffer, buffer_size); + if (frames > 0) { + int size = frames * bytes_per_frame; + encoder_encode_audio_frame(enc_ctx, buffer, size); + } + usleep(5000); + } + free(buffer); + return NULL; +} + +/* Video recording thread */ +void* record_thread_func(void* arg) { + int fps = gui_get_fps(gui); + int linesize = 0; + while (is_recording) { + if(rec_ctx && rec_ctx->is_window_capture) + recorder_update_window_geometry(rec_ctx); + uint8_t* frame_data = recorder_capture_frame(rec_ctx, &linesize); + if (frame_data) { + encoder_encode_video_frame(enc_ctx, frame_data); + free(frame_data); + } + usleep(1000000 / fps); + } + return NULL; +} + +/* Webcam preview thread */ +void* webcam_thread_func(void* arg) { + camera_thread_running = 1; + avdevice_register_all(); + AVFormatContext *fmt_ctx = NULL; + const AVInputFormat *input_fmt = av_find_input_format("v4l2"); + const char* device = "/dev/video0"; + AVDictionary *options = NULL; + const char* ws = gui_get_webcam_resolution(gui); + if (ws && strcmp(ws, "640x480") == 0) + av_dict_set(&options, "video_size", "640x480", 0); + if (avformat_open_input(&fmt_ctx, device, input_fmt, &options) != 0) { + fprintf(stderr, "Could not open webcam device\n"); + if (options) av_dict_free(&options); + camera_thread_running = 0; + return NULL; + } + if (options) av_dict_free(&options); + if (avformat_find_stream_info(fmt_ctx, NULL) < 0) { + fprintf(stderr, "Could not get stream info from webcam\n"); + avformat_close_input(&fmt_ctx); + camera_thread_running = 0; + return NULL; + } + int video_stream_index = av_find_best_stream(fmt_ctx, AVMEDIA_TYPE_VIDEO, -1, -1, NULL, 0); + if (video_stream_index < 0) { + fprintf(stderr, "Could not find video stream in webcam\n"); + avformat_close_input(&fmt_ctx); + camera_thread_running = 0; + return NULL; + } + const AVCodec *codec = avcodec_find_decoder(fmt_ctx->streams[video_stream_index]->codecpar->codec_id); + if (!codec) { + fprintf(stderr, "Could not find codec for webcam\n"); + avformat_close_input(&fmt_ctx); + camera_thread_running = 0; + return NULL; + } + AVCodecContext *codec_ctx = avcodec_alloc_context3(codec); + avcodec_parameters_to_context(codec_ctx, fmt_ctx->streams[video_stream_index]->codecpar); + if (avcodec_open2(codec_ctx, codec, NULL) < 0) { + fprintf(stderr, "Could not open webcam codec\n"); + avcodec_free_context(&codec_ctx); + avformat_close_input(&fmt_ctx); + camera_thread_running = 0; + return NULL; + } + struct SwsContext* sws_ctx = sws_getContext(codec_ctx->width, codec_ctx->height, codec_ctx->pix_fmt, + codec_ctx->width, codec_ctx->height, AV_PIX_FMT_RGB24, + SWS_BICUBIC, NULL, NULL, NULL); + AVPacket *packet = av_packet_alloc(); + AVFrame *frame = av_frame_alloc(); + AVFrame *rgb_frame = av_frame_alloc(); + int num_bytes = av_image_get_buffer_size(AV_PIX_FMT_RGB24, codec_ctx->width, codec_ctx->height, 1); + uint8_t *buffer_data = (uint8_t *) av_malloc(num_bytes * sizeof(uint8_t)); + av_image_fill_arrays(rgb_frame->data, rgb_frame->linesize, buffer_data, AV_PIX_FMT_RGB24, + codec_ctx->width, codec_ctx->height, 1); + + int sleep_duration = (ws && strcmp(ws, "640x480") == 0) ? 20000 : 30000; + while (camera_running) { + if (av_read_frame(fmt_ctx, packet) >= 0) { + if (packet->stream_index == video_stream_index) { + if (avcodec_send_packet(codec_ctx, packet) == 0) { + while (avcodec_receive_frame(codec_ctx, frame) == 0) { + sws_scale(sws_ctx, (const uint8_t* const*)frame->data, frame->linesize, 0, + codec_ctx->height, rgb_frame->data, rgb_frame->linesize); + GdkPixbuf *pixbuf = gdk_pixbuf_new_from_data(rgb_frame->data[0], + GDK_COLORSPACE_RGB, + FALSE, + 8, + codec_ctx->width, + codec_ctx->height, + rgb_frame->linesize[0], + NULL, NULL); + if (pixbuf) { + PreviewData *pd = malloc(sizeof(PreviewData)); + pd->image = gui->preview_area; + pd->pixbuf = g_object_ref(pixbuf); + g_idle_add(update_preview_idle, pd); + g_object_unref(pixbuf); + } + av_frame_unref(frame); + } + } + } + av_packet_unref(packet); + } + if (!camera_running) + break; + usleep(sleep_duration); + } + av_free(buffer_data); + av_frame_free(&rgb_frame); + av_frame_free(&frame); + av_packet_free(&packet); + sws_freeContext(sws_ctx); + avcodec_free_context(&codec_ctx); + avformat_close_input(&fmt_ctx); + camera_thread_running = 0; + return NULL; +} + +/* Get monitor geometry using XRandR */ +int get_monitor_geometry(const char* monitor_name, int *x, int *y, int *width, int *height) { + Display *dpy = XOpenDisplay(NULL); + if (!dpy) return -1; + Window root = DefaultRootWindow(dpy); + XRRScreenResources *res = XRRGetScreenResources(dpy, root); + if (!res) { + XCloseDisplay(dpy); + return -1; + } + int found = 0; + for (int i = 0; i < res->noutput; i++) { + XRROutputInfo *out = XRRGetOutputInfo(dpy, res, res->outputs[i]); + if (out && out->connection == RR_Connected && out->name && strcmp(out->name, monitor_name) == 0) { + if (out->crtc) { + XRRCrtcInfo *crtc = XRRGetCrtcInfo(dpy, res, out->crtc); + if (crtc) { + *x = crtc->x; + *y = crtc->y; + *width = crtc->width; + *height = crtc->height; + XRRFreeCrtcInfo(crtc); + found = 1; + XRRFreeOutputInfo(out); + break; + } + } + } + if (out) + XRRFreeOutputInfo(out); + } + XRRFreeScreenResources(res); + XCloseDisplay(dpy); + return found ? 0 : -1; +} + +/* Prompt for filename using a GTK dialog */ +static char* prompt_for_filename(GtkWindow *parent, const char *default_name) { + GtkWidget *dialog = gtk_dialog_new_with_buttons("Save Recording", + parent, + GTK_DIALOG_MODAL, + "_Save", GTK_RESPONSE_ACCEPT, + "_Cancel", GTK_RESPONSE_CANCEL, + NULL); + GtkWidget *content_area = gtk_dialog_get_content_area(GTK_DIALOG(dialog)); + GtkWidget *entry = gtk_entry_new(); + gtk_entry_set_text(GTK_ENTRY(entry), default_name); + gtk_container_add(GTK_CONTAINER(content_area), entry); + gtk_widget_show_all(dialog); + + char *result = NULL; + if (gtk_dialog_run(GTK_DIALOG(dialog)) == GTK_RESPONSE_ACCEPT) { + const char *text = gtk_entry_get_text(GTK_ENTRY(entry)); + if (text && strlen(text) > 0) + result = strdup(text); + } + gtk_widget_destroy(dialog); + return result; +} + +/* Callback for the recording toggle button */ +static void on_record_toggle(GtkToggleButton *toggle_button, gpointer user_data) { + if (gtk_toggle_button_get_active(toggle_button)) { + gtk_button_set_label(GTK_BUTTON(toggle_button), "Stop Recording"); + + /* Retrieve selections */ + RecordSource source = gui_get_record_source(gui); + Quality quality = gui_get_quality(gui); + const char* resolution_choice = gui_get_resolution(gui); + int capture_width, capture_height; + int cap_x = 0, cap_y = 0; + + /* Get full desktop dimensions */ + Display *dpy = XOpenDisplay(NULL); + if (!dpy) { + gtk_button_set_label(GTK_BUTTON(toggle_button), "Start Recording"); + return; + } + capture_width = DisplayWidth(dpy, DefaultScreen(dpy)); + capture_height = DisplayHeight(dpy, DefaultScreen(dpy)); + XCloseDisplay(dpy); + + /* Override resolution based on selection */ + if (strcmp(resolution_choice, "1080p") == 0) { + capture_width = 1920; capture_height = 1080; + } else if (strcmp(resolution_choice, "720p") == 0) { + capture_width = 1280; capture_height = 720; + } else if (strcmp(resolution_choice, "480p") == 0) { + capture_width = 854; capture_height = 480; + } + + Window target = 0; + if (source == RECORD_SOURCE_WINDOW) { + printf("Please click on the window you wish to record...\n"); + rec_ctx = recorder_init(0); + if (!rec_ctx) { + gtk_button_set_label(GTK_BUTTON(toggle_button), "Start Recording"); + return; + } + recorder_select_window(rec_ctx->display, &target, &rec_ctx->x, &rec_ctx->y, &capture_width, &capture_height); + recorder_cleanup(rec_ctx); + rec_ctx = recorder_init(target); + if (!rec_ctx) { + gtk_button_set_label(GTK_BUTTON(toggle_button), "Start Recording"); + return; + } + } else if (source == RECORD_SOURCE_MONITOR) { + const char* mon = gui_get_monitor_name(gui); + if (mon && get_monitor_geometry(mon, &cap_x, &cap_y, &capture_width, &capture_height) != 0) { + /* Handle geometry not found if necessary */ + } + rec_ctx = recorder_init(0); + if (!rec_ctx) { + gtk_button_set_label(GTK_BUTTON(toggle_button), "Start Recording"); + return; + } + rec_ctx->x = cap_x; + rec_ctx->y = cap_y; + } else { /* RECORD_SOURCE_ALL */ + rec_ctx = recorder_init(0); + if (!rec_ctx) { + gtk_button_set_label(GTK_BUTTON(toggle_button), "Start Recording"); + return; + } + } + if (capture_width % 2 != 0) capture_width--; + if (capture_height % 2 != 0) capture_height--; + rec_ctx->width = capture_width; + rec_ctx->height = capture_height; + recorder_start(rec_ctx); + + /* Retrieve FPS and audio settings */ + int fps = gui_get_fps(gui); + AudioCodec audio_codec = gui_get_audio_codec(gui); + int audio_bitrate = DEFAULT_AUDIO_BIT_RATE; + + enc_ctx = encoder_init(quality, capture_width, capture_height, fps, 44100, 2, audio_codec, audio_bitrate); + if (!enc_ctx) { + recorder_cleanup(rec_ctx); + gtk_button_set_label(GTK_BUTTON(toggle_button), "Start Recording"); + return; + } + audio_ctx = audio_init(); + audio_start(audio_ctx); + int audio_toggle_state = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(gui->audio_toggle)); + audio_set_capture(audio_ctx, audio_toggle_state); + + is_recording = 1; + recording_start_time = time(NULL); + pthread_create(&record_thread, NULL, record_thread_func, NULL); + pthread_create(&audio_thread, NULL, audio_thread_func, NULL); + g_timeout_add_seconds(1, update_info_callback, NULL); + } else { + gtk_button_set_label(GTK_BUTTON(toggle_button), "Start Recording"); + is_recording = 0; + recorder_stop(rec_ctx); + audio_stop(audio_ctx); + pthread_join(record_thread, NULL); + pthread_join(audio_thread, NULL); + encoder_finalize(enc_ctx); + + char original_fullpath[2048]; + { + const char *home = getenv("HOME"); + if (!home) home = "."; + char dirpath[1024]; + snprintf(dirpath, sizeof(dirpath), "%s/Videos/Screenrecords/", home); + snprintf(original_fullpath, sizeof(original_fullpath), "%s%s", dirpath, enc_ctx->filename); + } + char *new_basename = prompt_for_filename(GTK_WINDOW(gui->window), enc_ctx->filename); + if (new_basename) { + char new_fullpath[2048]; + if (new_basename[0] == '/') { + strncpy(new_fullpath, new_basename, sizeof(new_fullpath)-1); + new_fullpath[sizeof(new_fullpath)-1] = '\0'; + } else { + const char *home = getenv("HOME"); + if (!home) home = "."; + char dirpath[1024]; + snprintf(dirpath, sizeof(dirpath), "%s/Videos/Screenrecords/", home); + snprintf(new_fullpath, sizeof(new_fullpath), "%s%s", dirpath, new_basename); + } + if (strcmp(new_fullpath, original_fullpath) != 0) { + if (rename(original_fullpath, new_fullpath) != 0) { + perror("Error renaming file"); + } else { + strncpy(enc_ctx->filename, new_fullpath, sizeof(enc_ctx->filename)-1); + enc_ctx->filename[sizeof(enc_ctx->filename)-1] = '\0'; + } + } + free(new_basename); + } else { + if (remove(original_fullpath) != 0) { + perror("Error deleting file"); + } + gui_update_info(gui, "Recording cancelled and file deleted."); + } + recorder_cleanup(rec_ctx); + audio_cleanup(audio_ctx); + encoder_cleanup(enc_ctx); + rec_ctx = NULL; + audio_ctx = NULL; + enc_ctx = NULL; + } +} + +/* Callback for the camera toggle button */ +static void on_camera_toggle(GtkToggleButton *toggle_button, gpointer user_data) { + if (gtk_toggle_button_get_active(toggle_button)) { + gtk_button_set_label(GTK_BUTTON(toggle_button), "Camera Off"); + camera_running = 1; + if (pthread_create(&webcam_thread, NULL, webcam_thread_func, NULL) != 0) { + g_print("Error starting webcam preview thread\n"); + } + } else { + gtk_button_set_label(GTK_BUTTON(toggle_button), "Camera On"); + camera_running = 0; + while (camera_thread_running) + usleep(5000); + pthread_join(webcam_thread, NULL); + gtk_image_clear(GTK_IMAGE(gui->preview_area)); + gtk_widget_set_size_request(gui->preview_area, 0, 0); + } +} + +/* Callback for the audio toggle button */ +static void on_audio_toggle(GtkToggleButton *toggle_button, gpointer user_data) { + int state = gtk_toggle_button_get_active(toggle_button); + if (audio_ctx) + audio_set_capture(audio_ctx, state); + gtk_button_set_label(GTK_BUTTON(toggle_button), state ? "Audio On" : "Audio Off"); +} + +/* Print help message */ +static void print_help(const char *progname) { + printf("Usage: %s [OPTIONS]\n", progname); + printf("Options:\n"); + printf(" --help Display this help message and exit\n"); + printf(" --version Output version information and exit\n"); + printf(" --debug Enable additional debug output\n"); +} + +/* Parse command-line options using getopt_long */ +static void parse_options(int argc, char **argv) { + static struct option long_options[] = { + {"help", no_argument, 0, 'h'}, + {"version", no_argument, 0, 'v'}, + {"debug", no_argument, 0, 'd'}, + {0, 0, 0, 0} + }; + int opt; + while ((opt = getopt_long(argc, argv, "hvd", long_options, NULL)) != -1) { + switch (opt) { + case 'h': + print_help(argv[0]); + exit(0); + case 'v': + printf("%s version %s\n", argv[0], APP_VERSION); + exit(0); + case 'd': + g_debug = 1; + fprintf(stderr, "[DEBUG] Debug mode enabled\n"); + break; + default: + print_help(argv[0]); + exit(1); + } + } +} + +int main(int argc, char **argv) { + parse_options(argc, argv); + gtk_init(&argc, &argv); + gui = gui_init(); + if (!gui) + return 1; + g_signal_connect(gui->window, "destroy", G_CALLBACK(gtk_main_quit), NULL); + g_signal_connect(gui->record_toggle, "toggled", G_CALLBACK(on_record_toggle), NULL); + g_signal_connect(gui->camera_toggle, "toggled", G_CALLBACK(on_camera_toggle), NULL); + g_signal_connect(gui->audio_toggle, "toggled", G_CALLBACK(on_audio_toggle), NULL); + + if (g_debug) + fprintf(stderr, "[DEBUG] Entering main loop\n"); + gtk_main(); + if (g_debug) + fprintf(stderr, "[DEBUG] Exiting main loop\n"); + gui_cleanup(gui); + return 0; +} + diff --git a/src/recorder.c b/src/recorder.c new file mode 100644 index 0000000..e9456ad --- /dev/null +++ b/src/recorder.c @@ -0,0 +1,174 @@ +/* src/recorder.c */ +#include "recorder.h" +#include +#include +#include +#include +#include +#include + +/* + * Implements interactive window selection. + * Grabs the pointer, sets the cursor to a crosshair, waits for a button press, + * obtains the window under the pointer, and retrieves its geometry. + */ +void recorder_select_window(Display *display, Window *target, int *x, int *y, int *width, int *height) { + XEvent event; + Window ret_root, ret_child; + unsigned int uwidth, uheight; + unsigned int dummy_depth; + + /* Set crosshair cursor */ + Cursor cross_cursor = XCreateFontCursor(display, XC_crosshair); + XDefineCursor(display, DefaultRootWindow(display), cross_cursor); + + if (XGrabPointer(display, DefaultRootWindow(display), False, + ButtonPressMask, GrabModeSync, GrabModeAsync, + None, cross_cursor, CurrentTime) != GrabSuccess) { + fprintf(stderr, "Could not grab pointer for window selection\n"); + XFreeCursor(display, cross_cursor); + return; + } + + XAllowEvents(display, SyncPointer, CurrentTime); + XWindowEvent(display, DefaultRootWindow(display), ButtonPressMask, &event); + + ret_child = event.xbutton.subwindow; + if(ret_child == None) + ret_child = DefaultRootWindow(display); + + if (!XGetGeometry(display, ret_child, &ret_root, x, y, &uwidth, &uheight, &dummy_depth, &dummy_depth)) { + fprintf(stderr, "Failed to get geometry of the selected window\n"); + XUngrabPointer(display, CurrentTime); + XFreeCursor(display, cross_cursor); + return; + } + *width = (int)uwidth; + *height = (int)uheight; + *target = ret_child; + + XUngrabPointer(display, CurrentTime); + /* Restore default cursor */ + XUndefineCursor(display, DefaultRootWindow(display)); + XFreeCursor(display, cross_cursor); +} + +/* + * Initialize the recorder. + * If a nonzero target is provided, that window’s geometry is used; + * otherwise, the full screen (root) is captured. + */ +RecorderContext* recorder_init(Window target) { + RecorderContext *ctx = malloc(sizeof(RecorderContext)); + if (!ctx) return NULL; + ctx->display = XOpenDisplay(NULL); + if (!ctx->display) { + fprintf(stderr, "Could not open X display\n"); + free(ctx); + return NULL; + } + ctx->screen = DefaultScreen(ctx->display); + ctx->root = RootWindow(ctx->display, ctx->screen); + if (target) { + /* Capture a specific window */ + ctx->target = target; + ctx->is_window_capture = 1; + XWindowAttributes attr; + if (XGetWindowAttributes(ctx->display, target, &attr) == 0) { + fprintf(stderr, "Failed to get attributes for selected window\n"); + XCloseDisplay(ctx->display); + free(ctx); + return NULL; + } + ctx->x = attr.x; // For window capture, these values are relative to the window + ctx->y = attr.y; + ctx->width = attr.width; + ctx->height = attr.height; + } else { + /* Full screen capture */ + ctx->target = 0; + ctx->is_window_capture = 0; + ctx->x = 0; + ctx->y = 0; + ctx->width = DisplayWidth(ctx->display, ctx->screen); + ctx->height = DisplayHeight(ctx->display, ctx->screen); + } + ctx->is_capturing = 0; + return ctx; +} + +int recorder_start(RecorderContext* ctx) { + if (!ctx) return -1; + ctx->is_capturing = 1; + return 0; +} + +int recorder_stop(RecorderContext* ctx) { + if (!ctx) return -1; + ctx->is_capturing = 0; + return 0; +} + +void recorder_cleanup(RecorderContext* ctx) { + if (ctx) { + if (ctx->display) + XCloseDisplay(ctx->display); + free(ctx); + } +} + +/* + * Capture one frame from the screen (or target window) and convert it to 24-bit RGB. + * When capturing full screen (including monitor mode), uses ctx->x and ctx->y as the offset. + * For window capture, it captures starting at (0,0) as the window’s image. + */ +uint8_t* recorder_capture_frame(RecorderContext* ctx, int *linesize) { + if (!ctx || !ctx->is_capturing) + return NULL; + Window capture_win = ctx->is_window_capture ? ctx->target : ctx->root; + int x = ctx->is_window_capture ? 0 : ctx->x; + int y = ctx->is_window_capture ? 0 : ctx->y; + XImage *img = XGetImage(ctx->display, capture_win, x, y, ctx->width, ctx->height, AllPlanes, ZPixmap); + if (!img) { + fprintf(stderr, "Failed to capture screen image\n"); + return NULL; + } + int size = ctx->width * ctx->height * 3; + uint8_t *buffer = malloc(size); + if (!buffer) { + XDestroyImage(img); + return NULL; + } + for (int j = 0; j < ctx->height; j++) { + for (int i = 0; i < ctx->width; i++) { + unsigned long pixel = XGetPixel(img, i, j); + int index = (j * ctx->width + i) * 3; + buffer[index] = (pixel >> 16) & 0xff; // Red + buffer[index + 1] = (pixel >> 8) & 0xff; // Green + buffer[index + 2] = pixel & 0xff; // Blue + } + } + if (linesize) + *linesize = ctx->width * 3; + XDestroyImage(img); + return buffer; +} + +/* + * Dynamically updates the window geometry for window capture. + */ +int recorder_update_window_geometry(RecorderContext *ctx) { + if (!ctx || !ctx->is_window_capture) + return -1; + XWindowAttributes attr; + if (!XGetWindowAttributes(ctx->display, ctx->target, &attr)) { + fprintf(stderr, "Failed to update window attributes for dynamic tracking\n"); + return -1; + } + ctx->x = attr.x; + ctx->y = attr.y; + ctx->width = attr.width; + ctx->height = attr.height; + return 0; +} +