Add nvidia-like instant replay option
This commit is contained in:
parent
b2fc88336a
commit
c1f5d7ee4d
@ -2,9 +2,10 @@
|
|||||||
This is a screen recorder that has minimal impact on system performance by recording a window using the GPU only,
|
This is a screen recorder that has minimal impact on system performance by recording a window using the GPU only,
|
||||||
similar to shadowplay on windows. This is the fastest screen recording tool for Linux.
|
similar to shadowplay on windows. This is the fastest screen recording tool for Linux.
|
||||||
|
|
||||||
The output is an h264 encoded video with aac audio.
|
This screen recorder can be used for recording your desktop offline, for live streaming and for nvidia-like instant replay,
|
||||||
|
where only the last few seconds are saved.
|
||||||
|
|
||||||
This project is still early in development.
|
The output is an h264 encoded video with aac audio.
|
||||||
|
|
||||||
# Performance
|
# Performance
|
||||||
When recording a 4k game, fps drops from 30 to 7 when using OBS Studio, however when using this screen recorder
|
When recording a 4k game, fps drops from 30 to 7 when using OBS Studio, however when using this screen recorder
|
||||||
|
4
replay.sh
Executable file
4
replay.sh
Executable file
@ -0,0 +1,4 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
[ "$#" -ne 5 ] && echo "usage: replay.sh <window_id> <fps> <audio_input> <replay_time_sec> <output_file>" && exit 1
|
||||||
|
sibs build --release && ./sibs-build/linux_x86_64/release/gpu-screen-recorder -w "$1" -c mp4 -f "$2" -a "$3" -r "$4" -o "$5"
|
123
src/main.cpp
123
src/main.cpp
@ -52,9 +52,11 @@ extern "C" {
|
|||||||
#include <libavutil/hwcontext.h>
|
#include <libavutil/hwcontext.h>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#include <deque>
|
||||||
|
|
||||||
//#include <CL/cl.h>
|
//#include <CL/cl.h>
|
||||||
|
|
||||||
static char av_error_buffer[AV_ERROR_MAX_STRING_SIZE];
|
static thread_local char av_error_buffer[AV_ERROR_MAX_STRING_SIZE];
|
||||||
|
|
||||||
static char* av_error_to_string(int err) {
|
static char* av_error_to_string(int err) {
|
||||||
if(av_strerror(err, av_error_buffer, sizeof(av_error_buffer) < 0))
|
if(av_strerror(err, av_error_buffer, sizeof(av_error_buffer) < 0))
|
||||||
@ -301,6 +303,10 @@ std::vector<std::string> get_hardware_acceleration_device_names() {
|
|||||||
|
|
||||||
static void receive_frames(AVCodecContext *av_codec_context, AVStream *stream,
|
static void receive_frames(AVCodecContext *av_codec_context, AVStream *stream,
|
||||||
AVFormatContext *av_format_context,
|
AVFormatContext *av_format_context,
|
||||||
|
double replay_start_time,
|
||||||
|
std::deque<AVPacket*> &frame_data_queue,
|
||||||
|
int replay_buffer_size_secs,
|
||||||
|
bool &frames_erased,
|
||||||
std::mutex &write_output_mutex) {
|
std::mutex &write_output_mutex) {
|
||||||
AVPacket av_packet;
|
AVPacket av_packet;
|
||||||
av_init_packet(&av_packet);
|
av_init_packet(&av_packet);
|
||||||
@ -313,14 +319,30 @@ static void receive_frames(AVCodecContext *av_codec_context, AVStream *stream,
|
|||||||
av_packet_rescale_ts(&av_packet, av_codec_context->time_base,
|
av_packet_rescale_ts(&av_packet, av_codec_context->time_base,
|
||||||
stream->time_base);
|
stream->time_base);
|
||||||
av_packet.stream_index = stream->index;
|
av_packet.stream_index = stream->index;
|
||||||
|
av_packet.dts = AV_NOPTS_VALUE;
|
||||||
// Write the encoded video frame to disk
|
// Write the encoded video frame to disk
|
||||||
// av_write_frame(av_format_context, &av_packet)
|
// av_write_frame(av_format_context, &av_packet)
|
||||||
// write(STDOUT_FILENO, av_packet.data, av_packet.size)
|
// write(STDOUT_FILENO, av_packet.data, av_packet.size)
|
||||||
std::lock_guard<std::mutex> lock(write_output_mutex);
|
std::lock_guard<std::mutex> lock(write_output_mutex);
|
||||||
|
if(replay_buffer_size_secs != -1) {
|
||||||
|
double time_now = glfwGetTime();
|
||||||
|
double replay_time_elapsed = time_now - replay_start_time;
|
||||||
|
|
||||||
|
AVPacket *new_pack = new AVPacket();
|
||||||
|
av_packet_move_ref(new_pack, &av_packet);
|
||||||
|
frame_data_queue.push_back(new_pack);
|
||||||
|
if(replay_time_elapsed >= replay_buffer_size_secs) {
|
||||||
|
av_packet_unref(frame_data_queue.front());
|
||||||
|
delete frame_data_queue.front();
|
||||||
|
frame_data_queue.pop_front();
|
||||||
|
frames_erased = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
int ret = av_write_frame(av_format_context, &av_packet);
|
int ret = av_write_frame(av_format_context, &av_packet);
|
||||||
if(ret < 0) {
|
if(ret < 0) {
|
||||||
fprintf(stderr, "Error: Failed to write video frame to muxer, reason: %s (%d)\n", av_error_to_string(ret), ret);
|
fprintf(stderr, "Error: Failed to write video frame to muxer, reason: %s (%d)\n", av_error_to_string(ret), ret);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
av_packet_unref(&av_packet);
|
av_packet_unref(&av_packet);
|
||||||
} else if (res == AVERROR(EAGAIN)) { // we have no packet
|
} else if (res == AVERROR(EAGAIN)) { // we have no packet
|
||||||
// fprintf(stderr, "No packet!\n");
|
// fprintf(stderr, "No packet!\n");
|
||||||
@ -555,7 +577,16 @@ static void close_video(AVStream *video_stream, AVFrame *frame) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static void usage() {
|
static void usage() {
|
||||||
fprintf(stderr, "usage: gpu-screen-recorder -w <window_id> -c <container_format> -f <fps> [-a <audio_input>]\n");
|
fprintf(stderr, "usage: gpu-screen-recorder -w <window_id> -c <container_format> -f <fps> [-a <audio_input>] [-r <replay_buffer_size_sec>] [-o <output_file>]\n");
|
||||||
|
fprintf(stderr, "OPTIONS:\n");
|
||||||
|
fprintf(stderr, " -w Window to record.\n");
|
||||||
|
fprintf(stderr, " -c Container format for output file, for example mp4, or flv.\n");
|
||||||
|
fprintf(stderr, " -f Framerate to record at.\n");
|
||||||
|
fprintf(stderr, " -a Audio device to record from (pulse audio device). Optional, disabled by default.\n");
|
||||||
|
fprintf(stderr, " -r Replay buffer size in seconds. If this is set, then only the last seconds as set by this option will be stored"
|
||||||
|
" and the video will only be saved when the gpu-screen-recorder is closed. This feature is similar to Nvidia's instant replay feature."
|
||||||
|
" This option has be between 5 and 1200. Note that the replay buffer size will not always be precise, because of keyframes. Optional, disable by default.\n");
|
||||||
|
fprintf(stderr, " -o The output file path. If omitted, then the encoded data is sent to stdout.\n");
|
||||||
exit(1);
|
exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -577,7 +608,9 @@ int main(int argc, char **argv) {
|
|||||||
{ "-w", Arg { nullptr, false } },
|
{ "-w", Arg { nullptr, false } },
|
||||||
{ "-c", Arg { nullptr, false } },
|
{ "-c", Arg { nullptr, false } },
|
||||||
{ "-f", Arg { nullptr, false } },
|
{ "-f", Arg { nullptr, false } },
|
||||||
{ "-a", Arg { nullptr, true } }
|
{ "-a", Arg { nullptr, true } },
|
||||||
|
{ "-o", Arg { nullptr, true } },
|
||||||
|
{ "-r", Arg { nullptr, true} }
|
||||||
};
|
};
|
||||||
|
|
||||||
for(int i = 1; i < argc - 1; i += 2) {
|
for(int i = 1; i < argc - 1; i += 2) {
|
||||||
@ -604,10 +637,23 @@ int main(int argc, char **argv) {
|
|||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
const char *filename = "/dev/stdout";
|
const char *filename = args["-o"].value;
|
||||||
|
if(!filename)
|
||||||
|
filename = "/dev/stdout";
|
||||||
|
|
||||||
const double target_fps = 1.0 / (double)fps;
|
const double target_fps = 1.0 / (double)fps;
|
||||||
|
|
||||||
|
int replay_buffer_size_secs = -1;
|
||||||
|
const char *replay_buffer_size_secs_str = args["-r"].value;
|
||||||
|
if(replay_buffer_size_secs_str) {
|
||||||
|
replay_buffer_size_secs = atoi(replay_buffer_size_secs_str);
|
||||||
|
if(replay_buffer_size_secs < 5 || replay_buffer_size_secs > 1200) {
|
||||||
|
fprintf(stderr, "Error: option -r has to be between 5 and 1200, was: %s\n", replay_buffer_size_secs_str);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
replay_buffer_size_secs += 5; // Add a few seconds to account of lost packets because of non-keyframe packets skipped
|
||||||
|
}
|
||||||
|
|
||||||
Display *dpy = XOpenDisplay(nullptr);
|
Display *dpy = XOpenDisplay(nullptr);
|
||||||
if (!dpy) {
|
if (!dpy) {
|
||||||
fprintf(stderr, "Error: Failed to open display\n");
|
fprintf(stderr, "Error: Failed to open display\n");
|
||||||
@ -728,6 +774,9 @@ int main(int argc, char **argv) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//video_stream->duration = AV_TIME_BASE * 15;
|
||||||
|
//audio_stream->duration = AV_TIME_BASE * 15;
|
||||||
|
//av_format_context->duration = AV_TIME_BASE * 15;
|
||||||
int ret = avformat_write_header(av_format_context, nullptr);
|
int ret = avformat_write_header(av_format_context, nullptr);
|
||||||
if (ret < 0) {
|
if (ret < 0) {
|
||||||
fprintf(stderr, "Error occurred when opening output file: %s\n",
|
fprintf(stderr, "Error occurred when opening output file: %s\n",
|
||||||
@ -808,6 +857,10 @@ int main(int argc, char **argv) {
|
|||||||
std::mutex write_output_mutex;
|
std::mutex write_output_mutex;
|
||||||
std::thread audio_thread;
|
std::thread audio_thread;
|
||||||
|
|
||||||
|
double record_start_time = glfwGetTime();
|
||||||
|
std::deque<AVPacket*> frame_data_queue;
|
||||||
|
bool frames_erased = false;
|
||||||
|
|
||||||
SoundDevice sound_device;
|
SoundDevice sound_device;
|
||||||
Arg &audio_input_arg = args["-a"];
|
Arg &audio_input_arg = args["-a"];
|
||||||
if(audio_input_arg.value) {
|
if(audio_input_arg.value) {
|
||||||
@ -820,7 +873,7 @@ int main(int argc, char **argv) {
|
|||||||
uint8_t *audio_frame_buf = (uint8_t *)av_malloc(audio_buffer_size);
|
uint8_t *audio_frame_buf = (uint8_t *)av_malloc(audio_buffer_size);
|
||||||
avcodec_fill_audio_frame(audio_frame, audio_stream->codec->channels, audio_stream->codec->sample_fmt, (const uint8_t*)audio_frame_buf, audio_buffer_size, 1);
|
avcodec_fill_audio_frame(audio_frame, audio_stream->codec->channels, audio_stream->codec->sample_fmt, (const uint8_t*)audio_frame_buf, audio_buffer_size, 1);
|
||||||
|
|
||||||
audio_thread = std::thread([audio_buffer_size](AVFormatContext *av_format_context, AVStream *audio_stream, uint8_t *audio_frame_buf, SoundDevice *sound_device, AVFrame *audio_frame, std::mutex *write_output_mutex) mutable {
|
audio_thread = std::thread([audio_buffer_size, record_start_time, replay_buffer_size_secs, &frame_data_queue, &frames_erased](AVFormatContext *av_format_context, AVStream *audio_stream, uint8_t *audio_frame_buf, SoundDevice *sound_device, AVFrame *audio_frame, std::mutex *write_output_mutex) mutable {
|
||||||
AVPacket audio_packet;
|
AVPacket audio_packet;
|
||||||
if(av_new_packet(&audio_packet, audio_buffer_size) != 0) {
|
if(av_new_packet(&audio_packet, audio_buffer_size) != 0) {
|
||||||
fprintf(stderr, "Failed to create audio packet\n");
|
fprintf(stderr, "Failed to create audio packet\n");
|
||||||
@ -862,8 +915,25 @@ int main(int argc, char **argv) {
|
|||||||
//printf("Succeed to encode 1 frame! \tsize:%5d\n",pkt.size);
|
//printf("Succeed to encode 1 frame! \tsize:%5d\n",pkt.size);
|
||||||
audio_packet.stream_index = audio_stream->index;
|
audio_packet.stream_index = audio_stream->index;
|
||||||
std::lock_guard<std::mutex> lock(*write_output_mutex);
|
std::lock_guard<std::mutex> lock(*write_output_mutex);
|
||||||
|
if(replay_buffer_size_secs != -1) {
|
||||||
|
double time_now = glfwGetTime();
|
||||||
|
double replay_time_elapsed = time_now - record_start_time;
|
||||||
|
AVPacket *new_pack = new AVPacket();
|
||||||
|
av_packet_move_ref(new_pack, &audio_packet);
|
||||||
|
frame_data_queue.push_back(new_pack);
|
||||||
|
if(replay_time_elapsed >= replay_buffer_size_secs) {
|
||||||
|
av_packet_unref(frame_data_queue.front());
|
||||||
|
delete frame_data_queue.front();
|
||||||
|
frame_data_queue.pop_front();
|
||||||
|
frames_erased = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
ret = av_write_frame(av_format_context, &audio_packet);
|
ret = av_write_frame(av_format_context, &audio_packet);
|
||||||
av_free_packet(&audio_packet);
|
if(ret < 0) {
|
||||||
|
fprintf(stderr, "Error: Failed to write audio frame to muxer, reason: %s (%d)\n", av_error_to_string(ret), ret);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
av_packet_unref(&audio_packet);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
fprintf(stderr, "failed to read sound from device, error: %d\n", sound_buffer_size);
|
fprintf(stderr, "failed to read sound from device, error: %d\n", sound_buffer_size);
|
||||||
@ -988,8 +1058,8 @@ int main(int argc, char **argv) {
|
|||||||
frame->pts = frame_count;
|
frame->pts = frame_count;
|
||||||
frame_count += 1;
|
frame_count += 1;
|
||||||
if (avcodec_send_frame(video_stream->codec, frame) >= 0) {
|
if (avcodec_send_frame(video_stream->codec, frame) >= 0) {
|
||||||
receive_frames(video_stream->codec, video_stream,
|
receive_frames(video_stream->codec, video_stream, av_format_context,
|
||||||
av_format_context, write_output_mutex);
|
record_start_time, frame_data_queue, replay_buffer_size_secs, frames_erased, write_output_mutex);
|
||||||
} else {
|
} else {
|
||||||
fprintf(stderr, "Error: avcodec_send_frame failed\n");
|
fprintf(stderr, "Error: avcodec_send_frame failed\n");
|
||||||
}
|
}
|
||||||
@ -1010,6 +1080,43 @@ int main(int argc, char **argv) {
|
|||||||
sound_device_close(&sound_device);
|
sound_device_close(&sound_device);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if(replay_buffer_size_secs != -1) {
|
||||||
|
size_t start_index = 0;
|
||||||
|
for(size_t i = 0; i < frame_data_queue.size(); ++i) {
|
||||||
|
AVPacket *av_packet = frame_data_queue[i];
|
||||||
|
if((av_packet->flags & AV_PKT_FLAG_KEY) && av_packet->stream_index == video_stream->index) {
|
||||||
|
start_index = i;
|
||||||
|
break;
|
||||||
|
} else {
|
||||||
|
//av_packet_unref(av_packet);
|
||||||
|
//delete av_packet;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//fprintf(stderr, "Frame start index: %zu\n", start_index);
|
||||||
|
|
||||||
|
int64_t pts_offset = 0;
|
||||||
|
if(frames_erased)
|
||||||
|
pts_offset = frame_data_queue[start_index]->pts;
|
||||||
|
|
||||||
|
for(size_t i = start_index; i < frame_data_queue.size(); ++i) {
|
||||||
|
AVPacket *av_packet = frame_data_queue[i];
|
||||||
|
if(av_packet->stream_index == video_stream->index) {
|
||||||
|
av_packet->pos = -1;
|
||||||
|
av_packet->pts -= pts_offset;
|
||||||
|
av_packet->dts = AV_NOPTS_VALUE;
|
||||||
|
}
|
||||||
|
av_packet->pos = -1;
|
||||||
|
int ret = av_write_frame(av_format_context, av_packet);
|
||||||
|
if(ret < 0) {
|
||||||
|
fprintf(stderr, "Error: Failed to write video frame to muxer, reason: %s (%d)\n", av_error_to_string(ret), ret);
|
||||||
|
}
|
||||||
|
//av_packet_unref(av_packet);
|
||||||
|
//delete av_packet;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
//Flush Encoder
|
//Flush Encoder
|
||||||
#if 0
|
#if 0
|
||||||
ret = flush_encoder(pFormatCtx,0);
|
ret = flush_encoder(pFormatCtx,0);
|
||||||
|
Loading…
Reference in New Issue
Block a user