Because I feel that some recording software doesn't meet my needs, I need to control the recording parameters of ffmpeg myself, and I also want it to have hotkey functionality so I can quickly start and stop recording at any time, so I decided to develop a small tool myself.
I installed Screen Capture Recorder, which is a free open-source project based on DirectShow and can provide screen and audio sources for FFMPEG. Its SourceForge address is:https://sourceforge.net/projects/screencapturer/files/
I used the virtual audio device it provides, 'virtual-audio-capturer', so my screen recording command became
ffmpeg -f gdigrab -framerate 60 -video_size 1600x900 -i desktop -f dshow -i audio="virtual-audio-capturer" -c:v libx264 -b:v 4M -preset ultrafast -pix_fmt yuv420p -c:a aac -b:a 196k -shortest out.mp4
I used another open-source library, subprocess.h, to take over the program's input and output: https://github.com/sheredom/subprocess.h , this way I can control when ffmpeg stops recording
You can implement hotkey trigger functionality by listening to users' hotkeys through the Windows message loop.
Based on the above basics, this feature can be realized. My program code is:
#include <cstdio>
#include <cstdlib>
#include <iostream>
#include <thread>
#include <chrono>
#include <windows.h>
#include <string.h>
#include "subprocess.h"
#define HOTKEY_ID 101
#define HOTKEY_ID_CANCEL 111
// Background thread: read and print FFmpeg stdout/stderr
void output_reader(struct subprocess_s* process) {
char buffer[1024];
while (1) {
unsigned int bytes_read = 0;
// Read stderr (FFmpeg progress usually goes here)
bytes_read = subprocess_read_stderr(process, buffer, sizeof(buffer) - 1);
if (bytes_read > 0) {
buffer[bytes_read] = '\0';
std::cerr << buffer << std::flush;
}
if (!subprocess_alive(process)) {
break;
}
// Small sleep to avoid busy-waiting
std::this_thread::sleep_for(std::chrono::milliseconds(30));
}
}
int main() {
if (!RegisterHotKey(NULL, HOTKEY_ID, MOD_CONTROL | MOD_ALT, 'R')) {
std::cerr << "Hotkey registered failed: " << GetLastError() << std::endl;
return 1;
}
if (!RegisterHotKey(NULL, HOTKEY_ID_CANCEL, MOD_CONTROL | MOD_ALT, 'B')) {
std::cerr << "Hotkey registered failed: " << GetLastError() << std::endl;
return 1;
}
// Fixed FFmpeg command
const char* command_line_0[] = {
"ffmpeg",
"-y",
"-f","gdigrab",
"-framerate","60",
"-video_size","1600x900",
"-i","desktop",
"-f","dshow",
"-i","audio=virtual-audio-capturer",
"-c:v","libx264",
"-b:v","6M",
"-preset","ultrafast",
"-pix_fmt","yuv420p",
"-c:a","aac",
"-b:a","196k",
"-shortest",
"screen_recording.mp4",
NULL
};
const char* command_line_1[] = {
"ffmpeg",
"-y",
"-f","gdigrab",
"-framerate","30",
"-video_size","1600x900",
"-i","desktop",
"-f","dshow",
"-i","audio=virtual-audio-capturer",
"-c:v","libx264",
"-b:v","4M",
"-preset","ultrafast",
"-pix_fmt","yuv420p",
"-c:a","aac",
"-b:a","196k",
"-shortest",
"screen_recording.mp4",
NULL
};
char **pCmd = NULL;
while(1){
std::cout<<"'1' capture screen 60FPS 6M;\n'2' capture screen 30FPS 4M;\n'q' to quit;\ninput choice"<<std::endl;
char buf[100];
std::cin.getline(buf, 100);
if(!strcmp(buf,"1")){
pCmd = (char**)command_line_0;
}else if(!strcmp(buf,"2")){
pCmd = (char**)command_line_1;
}else if(!strcmp(buf,"q")){
break;
}else{
continue;
}
std::cout<<"Ctrl+Alt+R: start/stop Recording, Ctrl+Alt+B: back to menu."<<std::endl;
MSG msg = {0};
int operationType;
while (GetMessage(&msg, NULL, 0, 0) != 0) {
if (msg.message == WM_HOTKEY) {
if (msg.wParam == HOTKEY_ID) {
operationType = 0;
break;
}else if (msg.wParam == HOTKEY_ID_CANCEL) {
operationType = 1;
break;
}
}
}
if(operationType == 1){
continue;
}
struct subprocess_s process;
// Enable async option so we can read output while the process runs
int result = subprocess_create(pCmd, subprocess_option_enable_async | subprocess_option_inherit_environment, &process);
if (result != 0) {
std::cerr << "Failed to start FFmpeg, error code: " << result << std::endl;
break;
}
// Start the background reader thread
std::thread reader_thread(output_reader, &process);
std::cout << "FFmpeg is working... Press Hotkey to send 'q' to ffmpeg and quit gracefully.\n";
while (GetMessage(&msg, NULL, 0, 0) != 0) {
if (msg.message == WM_HOTKEY) {
if (msg.wParam == HOTKEY_ID) {
// Send 'q' to FFmpeg's stdin
FILE* p_stdin = subprocess_stdin(&process);
if (p_stdin) {
fputs("q", p_stdin);
fflush(p_stdin);
break;
}
}
}
}
// Wait for FFmpeg to exit
int process_return = 0;
subprocess_join(&process, &process_return);
std::cout << "\nFFmpeg exited with return code: " << process_return << std::endl;
// Join the reader thread
if (reader_thread.joinable()) {
reader_thread.join();
}
subprocess_destroy(&process);
}
UnregisterHotKey(NULL, HOTKEY_ID);
UnregisterHotKey(NULL, HOTKEY_ID_CANCEL);
return 0;
}
The output after compiling and running:
'1' capture screen 60FPS 6M;
'2' capture screen 30FPS 4M;
'q' to quit;
input choice
1
Ctrl+Alt+R: start/stop Recording, Ctrl+Alt+B: back to menu.
First, choose a menu. There are two recording options. After selecting one, it will show the shortcut keys for how to start/stop recording or go back to the menu to choose a different recording option.
Pressing the shortcut key to start recording, it will show the information output from the ffmpeg recording process.
FFmpeg is working... Press Hotkey to send 'q' to ffmpeg and quit gracefully.
ffmpeg version 4.2.2 Copyright (c) 2000-2019 the FFmpeg developers
built with gcc 9.2.1 (GCC) 20200122
configuration: --enable-gpl --enable-version3 --enable-sdl2 --enable-fontconfig --enable-gnutls --enable-iconv --enable-libass --enable-libdav1d --enable-libbluray --enable-libfreetype --enable-libmp3lame --enable-libopencore-amrnb --enable-libopencore-amrwb --enable-libopenjpeg --enable-libopus --enable-libshine --enable-libsnappy --enable-libsoxr --enable-libtheora --enable-libtwolame --enable-libvpx --enable-libwavpack --enable-libwebp --enable-libx264 --enable-libx265 --enable-libxml2 --enable-libzimg --enable-lzma --enable-zlib --enable-gmp --enable-libvidstab --enable-libvorbis --enable-libvo-amrwbenc --enable-libmysofa --enable-libspeex --enable-libxvid --enable-libaom --enable-libmfx --enable-amf --enable-ffnvcodec --enable-cuvid --enable-d3d11va --enable-nvenc --enable-nvdec --enable-dxva2 --enable-avisynth --enable-libopenmpt
...
frame= 40 fps=0.0 q=31.0 size= 512kB time=00:00:00.44 bitrate=9363.1kbits/s dup=18 drframe= 70 fps= 69 q=21.0 size= 512kB time=00:00:00.96 bitrate=4364.9kbits/s dup=18 drframe= 100 fps= 66 q=17.0 size= 768kB time=00:00:01.45 bitrate=4334.2kbits/s dup=19
Pressing the shortcut key to stop recording will bring you back to the menu
[libx264 @ 061aabc0] final ratefactor: 8.84
[libx264 @ 061aabc0] coded y,uvDC,uvAC intra: 38.6% 39.7% 38.5% inter: 1.9% 3.1% 2.9%
[libx264 @ 061aabc0] i16 v,h,dc,p: 61% 32% 5% 2%
[libx264 @ 061aabc0] i8c dc,h,v,p: 53% 23% 23% 1%
[libx264 @ 061aabc0] kb/s:4980.81
[aac @ 061adf80] Qavg: 174.372
FFmpeg exited with return code: 0
'1' capture screen 60FPS 6M;
'2' capture screen 30FPS 4M;
'q' to quit;
input choice
I updated this program, and now it can output filenames with the recording time, for example: Recording_2026-10-04_00-37-20.mp4
#include <chrono>
#include <ctime>
#include <iomanip>
#include <sstream>
#include <string>
std::string get_current_time_string() {
auto now = std::chrono::system_clock::now();
std::time_t t = std::chrono::system_clock::to_time_t(now);
std::tm tm_buf{};
#if defined(_WIN32)
localtime_s(&tm_buf, &t); // Windows thread safe version
#else
localtime_r(&t, &tm_buf); // POSIX thread safe version
#endif
std::ostringstream oss;
oss << std::put_time(&tm_buf, "%Y-%m-%d_%H-%M-%S");
return oss.str();
}
#include <cstdio>
#include <cstdlib>
#include <iostream>
#include <thread>
#include <windows.h>
#include <chrono>
#include <string.h>
#include "subprocess.h"
#define HOTKEY_ID 101
#define HOTKEY_ID_CANCEL 111
// Background thread: read and print FFmpeg stdout/stderr
void output_reader(struct subprocess_s* process) {
char buffer[1024];
while (1) {
unsigned int bytes_read = 0;
// Read stderr (FFmpeg progress usually goes here)
bytes_read = subprocess_read_stderr(process, buffer, sizeof(buffer) - 1);
if (bytes_read > 0) {
buffer[bytes_read] = '\0';
std::cerr << buffer << std::flush;
}
if (!subprocess_alive(process)) {
break;
}
// Small sleep to avoid busy-waiting
std::this_thread::sleep_for(std::chrono::milliseconds(30));
}
}
int main() {
if (!RegisterHotKey(NULL, HOTKEY_ID, MOD_CONTROL | MOD_ALT, 'R')) {
std::cerr << "Hotkey registered failed: " << GetLastError() << std::endl;
return 1;
}
if (!RegisterHotKey(NULL, HOTKEY_ID_CANCEL, MOD_CONTROL | MOD_ALT, 'B')) {
std::cerr << "Hotkey registered failed: " << GetLastError() << std::endl;
return 1;
}
// Fixed FFmpeg command
char timeBuf[64] = {0};
const char* command_line_0[] = {
"ffmpeg",
"-y",
"-f","gdigrab",
"-framerate","60",
"-video_size","1600x900",
"-i","desktop",
"-f","dshow",
"-i","audio=virtual-audio-capturer",
"-c:v","libx264",
"-b:v","8M",
"-preset","ultrafast",
"-pix_fmt","yuv420p",
"-c:a","aac",
"-b:a","196k",
"-shortest",
timeBuf,
NULL
};
const char* command_line_1[] = {
"ffmpeg",
"-y",
"-f","gdigrab",
"-framerate","30",
"-video_size","1600x900",
"-i","desktop",
"-f","dshow",
"-i","audio=virtual-audio-capturer",
"-c:v","libx264",
"-b:v","6M",
"-preset","ultrafast",
"-pix_fmt","yuv420p",
"-c:a","aac",
"-b:a","196k",
"-shortest",
timeBuf,
NULL
};
char **pCmd = NULL;
while(1){
std::cout<<"'1' capture screen 60FPS 8M;\n'2' capture screen 30FPS 6M;\n'q' to quit;\ninput choice"<<std::endl;
char buf[100];
std::cin.getline(buf, 100);
if(!strcmp(buf,"1")){
pCmd = (char**)command_line_0;
}else if(!strcmp(buf,"2")){
pCmd = (char**)command_line_1;
}else if(!strcmp(buf,"q")){
break;
}else{
continue;
}
std::cout<<"Ctrl+Alt+R: start/stop Recording, Ctrl+Alt+B: back to menu."<<std::endl;
MSG msg = {0};
int operationType;
while (GetMessage(&msg, NULL, 0, 0) != 0) {
if (msg.message == WM_HOTKEY) {
if (msg.wParam == HOTKEY_ID) {
operationType = 0;
break;
}else if (msg.wParam == HOTKEY_ID_CANCEL) {
operationType = 1;
break;
}
}
}
if(operationType == 1){
continue;
}
std::string currentTime = get_current_time_string();
memset(timeBuf,0,sizeof(timeBuf));
snprintf(timeBuf, sizeof(timeBuf), "Recording_%s.mp4", currentTime.c_str());
struct subprocess_s process;
// Enable async option so we can read output while the process runs
int result = subprocess_create(pCmd, subprocess_option_enable_async | subprocess_option_inherit_environment, &process);
if (result != 0) {
std::cerr << "Failed to start FFmpeg, error code: " << result << std::endl;
break;
}
// Start the background reader thread
std::thread reader_thread(output_reader, &process);
std::cout << "FFmpeg is working... Press Hotkey to send 'q' to ffmpeg and quit gracefully.\n";
while (GetMessage(&msg, NULL, 0, 0) != 0) {
if (msg.message == WM_HOTKEY) {
if (msg.wParam == HOTKEY_ID) {
// Send 'q' to FFmpeg's stdin
FILE* p_stdin = subprocess_stdin(&process);
if (p_stdin) {
fputs("q", p_stdin);
fflush(p_stdin);
break;
}
}
}
}
// Wait for FFmpeg to exit
int process_return = 0;
subprocess_join(&process, &process_return);
std::cout << "\nFFmpeg exited with return code: " << process_return << std::endl;
// Join the reader thread
if (reader_thread.joinable()) {
reader_thread.join();
}
subprocess_destroy(&process);
}
UnregisterHotKey(NULL, HOTKEY_ID);
UnregisterHotKey(NULL, HOTKEY_ID_CANCEL);
return 0;
}Some games can cause ffmpeg to output error messages when recording start.
FFmpeg exited with return code: 1
This might be because the preset resolution doesn't match the game's resolution. So you just need to add the game's resolution option in the preset scheme.
If you want to output to a specific directory, you just need to change the path in ffmpeg's output file. For example:
snprintf(timeBuf, sizeof(timeBuf), "D:/Record Videos/Recording_%s.mp4", currentTime.c_str());