aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authordec05eba <dec05eba@protonmail.com>2026-08-01 23:02:07 +0200
committerdec05eba <dec05eba@protonmail.com>2026-08-01 23:02:07 +0200
commit40b6a13d14680cfacb3cdb59f04224540b80d9e4 (patch)
treed50c5eb6ae5de196e9ae90207bc172f719169866
parentbd12a0ede46bbd92510f1cd82b04383e4a9e83c8 (diff)
Move muxing and replay saving from main.cpp into muxer and replay_save modules
Replay saving uses a pthread with an owning result struct instead of std::future and a global output filepath.
-rw-r--r--include/recorder/muxer.h40
-rw-r--r--include/recorder/replay_save.h53
-rw-r--r--meson.build2
-rw-r--r--src/main.cpp420
-rw-r--r--src/recorder/muxer.c268
-rw-r--r--src/recorder/replay_save.c207
6 files changed, 608 insertions, 382 deletions
diff --git a/include/recorder/muxer.h b/include/recorder/muxer.h
new file mode 100644
index 0000000..e47cdde
--- /dev/null
+++ b/include/recorder/muxer.h
@@ -0,0 +1,40 @@
+#ifndef GSR_RECORDER_MUXER_H
+#define GSR_RECORDER_MUXER_H
+
+#include <stdbool.h>
+#include <stddef.h>
+#include <limits.h>
+#include "audio_capture.h"
+#include "capture_setup.h"
+#include "settings.h"
+
+#include <libavformat/avformat.h>
+
+typedef struct {
+ const gsr_audio_track *audio_track;
+ AVStream *stream;
+} gsr_recording_audio_stream;
+
+typedef struct {
+ AVFormatContext *av_format_context;
+ AVStream *video_stream;
+ gsr_recording_audio_stream *audio_streams;
+ size_t num_audio_streams;
+} gsr_recording_output;
+
+AVStream* create_stream(AVFormatContext *av_format_context, AVCodecContext *codec_context);
+bool add_hdr_metadata_to_video_stream(gsr_capture *cap, AVStream *video_stream);
+void set_format_context_options(AVFormatContext *av_format_context);
+void av_write_header(AVFormatContext *av_format_context, const char *ffmpeg_opts);
+
+/* Returns false on failure. Creates the output file and its video and audio streams */
+bool gsr_recording_output_start(gsr_recording_output *self, const char *filename, const gsr_recorder_settings *settings, AVCodecContext *video_codec_context, const gsr_audio_capture *audio_capture, bool hdr, gsr_video_sources *video_sources);
+bool gsr_recording_output_stop(gsr_recording_output *self);
+gsr_recording_audio_stream* gsr_recording_output_get_audio_stream_by_index(gsr_recording_output *self, int stream_index);
+
+/* |filepath| should be at least PATH_MAX bytes in size. Creates the directories of the filepath */
+bool gsr_create_new_recording_filepath_from_timestamp(char *filepath, size_t filepath_size, const char *directory, const char *filename_prefix, const char *file_extension, bool date_folders);
+
+size_t calculate_estimated_replay_buffer_packets(int64_t replay_buffer_size_secs, int fps, gsr_audio_codec audio_codec, const gsr_audio_input_tracks *audio_inputs);
+
+#endif /* GSR_RECORDER_MUXER_H */
diff --git a/include/recorder/replay_save.h b/include/recorder/replay_save.h
new file mode 100644
index 0000000..d92a8c0
--- /dev/null
+++ b/include/recorder/replay_save.h
@@ -0,0 +1,53 @@
+#ifndef GSR_RECORDER_REPLAY_SAVE_H
+#define GSR_RECORDER_REPLAY_SAVE_H
+
+#include <stdbool.h>
+#include <stddef.h>
+#include <limits.h>
+#include <pthread.h>
+#include <signal.h>
+#include "muxer.h"
+#include "audio_capture.h"
+#include "capture_setup.h"
+#include "settings.h"
+
+#include "../encoder/encoder.h"
+#include "../replay_buffer/replay_buffer.h"
+
+#define GSR_SAVE_REPLAY_SECONDS_FULL -1
+
+typedef struct {
+ int64_t pts_offset;
+ int stream_index;
+} gsr_audio_pts_offset;
+
+/* Saves the replay buffer to a file on a separate thread */
+typedef struct {
+ pthread_t thread;
+ bool thread_created;
+ volatile sig_atomic_t finished;
+ bool success;
+ char output_filepath[PATH_MAX];
+
+ AVCodecContext *video_codec_context;
+ int video_stream_index;
+ gsr_recording_output recording_output;
+ gsr_replay_buffer_iterator video_start_iterator;
+ int64_t video_pts_offset;
+ gsr_audio_pts_offset *audio_pts_offsets;
+ size_t num_audio_pts_offsets;
+ gsr_replay_buffer *cloned_replay_buffer;
+ gsr_encoder *encoder;
+} gsr_replay_save;
+
+void gsr_replay_save_init(gsr_replay_save *self);
+bool gsr_replay_save_is_running(const gsr_replay_save *self);
+
+/* Returns false if the replay failed to start. |current_save_replay_seconds| can be GSR_SAVE_REPLAY_SECONDS_FULL to save the whole replay buffer */
+bool gsr_replay_save_start(gsr_replay_save *self, AVCodecContext *video_codec_context, int video_stream_index, const gsr_audio_capture *audio_capture, gsr_encoder *encoder, const gsr_recorder_settings *settings, const char *file_extension, bool hdr, gsr_video_sources *video_sources, int current_save_replay_seconds);
+/* Returns true when the replay finished saving, in which case |success| and |output_filepath| are set. |output_filepath| is empty when nothing was saved */
+bool gsr_replay_save_poll(gsr_replay_save *self, bool *success, const char **output_filepath);
+/* Waits for an ongoing replay save to finish. Returns the same values as gsr_replay_save_poll */
+bool gsr_replay_save_join(gsr_replay_save *self, bool *success, const char **output_filepath);
+
+#endif /* GSR_RECORDER_REPLAY_SAVE_H */
diff --git a/meson.build b/meson.build
index d56b21c..3b455a4 100644
--- a/meson.build
+++ b/meson.build
@@ -41,6 +41,8 @@ src = [
'src/recorder/audio_input.c',
'src/recorder/audio_capture.c',
'src/recorder/recording_clock.c',
+ 'src/recorder/muxer.c',
+ 'src/recorder/replay_save.c',
'src/egl.c',
'src/cuda.c',
'src/window_texture.c',
diff --git a/src/main.cpp b/src/main.cpp
index b3056c3..6a030aa 100644
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -8,6 +8,8 @@ extern "C" {
#include "../include/recorder/capture_setup.h"
#include "../include/recorder/audio_input.h"
#include "../include/recorder/audio_capture.h"
+#include "../include/recorder/muxer.h"
+#include "../include/recorder/replay_save.h"
#include "../include/recorder/error.h"
#include "../include/capture/nvfbc.h"
#include "../include/capture/xcomposite.h"
@@ -89,8 +91,6 @@ static const int VIDEO_STREAM_INDEX = 0;
/* AV1/VP9 use the 0-255 qindex quantizer scale and VP8 uses 0-127. Unlike h264 qp the quantizer step size
is not exponential in qindex, so the mapping to an equivalent h264 qp is not linear in the quantizer range.
qindex = h264 qp * 4 matches the qindex = crf * 4 scale used by libvpx/libaom, where crf is roughly equivalent to h264 qp. */
-static const int save_replay_seconds_full = -1;
-
static sig_atomic_t running = 1;
static sig_atomic_t toggle_pause = 0;
static sig_atomic_t toggle_replay_recording = 0;
@@ -109,7 +109,7 @@ static void toggle_replay_recording_handler(int) {
}
static void save_replay_handler(int) {
- save_replay_seconds = save_replay_seconds_full;
+ save_replay_seconds = GSR_SAVE_REPLAY_SECONDS_FULL;
}
static void save_replay_10_seconds_handler(int) {
@@ -136,19 +136,6 @@ static void save_replay_30_minutes_handler(int) {
save_replay_seconds = 60*30;
}
-static AVStream* create_stream(AVFormatContext *av_format_context, AVCodecContext *codec_context) {
- AVStream *stream = avformat_new_stream(av_format_context, nullptr);
- if (!stream) {
- gsr_log(GSR_LOG_LEVEL_ERROR, "Could not allocate stream");
- _exit(1);
- }
- stream->id = av_format_context->nb_streams - 1;
- stream->time_base = codec_context->time_base;
- stream->avg_frame_rate = codec_context->framerate;
- //stream->r_frame_rate = codec_context->framerate;
- return stream;
-}
-
static void run_recording_saved_script_async(const char *script_file, const char *video_file, const char *type) {
char script_file_full[PATH_MAX];
script_file_full[0] = '\0';
@@ -197,318 +184,6 @@ static void run_recording_saved_script_async(const char *script_file, const char
}
// TODO: Cleanup
-static bool add_hdr_metadata_to_video_stream(gsr_capture *cap, AVStream *video_stream) {
- size_t light_metadata_size = 0;
- size_t mastering_display_metadata_size = 0;
- AVContentLightMetadata *light_metadata = av_content_light_metadata_alloc(&light_metadata_size);
- #if LIBAVUTIL_VERSION_INT < AV_VERSION_INT(59, 37, 100)
- AVMasteringDisplayMetadata *mastering_display_metadata = av_mastering_display_metadata_alloc();
- mastering_display_metadata_size = sizeof(*mastering_display_metadata);
- #else
- AVMasteringDisplayMetadata *mastering_display_metadata = av_mastering_display_metadata_alloc_size(&mastering_display_metadata_size);
- #endif
-
- if(!light_metadata || !mastering_display_metadata) {
- if(light_metadata)
- av_freep(&light_metadata);
-
- if(mastering_display_metadata)
- av_freep(&mastering_display_metadata);
-
- return false;
- }
-
- if(!gsr_capture_set_hdr_metadata(cap, mastering_display_metadata, light_metadata)) {
- av_freep(&light_metadata);
- av_freep(&mastering_display_metadata);
- return false;
- }
-
- // TODO: More error checking
-
- #if LIBAVCODEC_VERSION_INT < AV_VERSION_INT(60, 31, 102)
- const bool content_light_level_added = av_stream_add_side_data(video_stream, AV_PKT_DATA_CONTENT_LIGHT_LEVEL, (uint8_t*)light_metadata, light_metadata_size) == 0;
- #else
- const bool content_light_level_added = av_packet_side_data_add(&video_stream->codecpar->coded_side_data, &video_stream->codecpar->nb_coded_side_data, AV_PKT_DATA_CONTENT_LIGHT_LEVEL, light_metadata, light_metadata_size, 0) != NULL;
- #endif
-
- #if LIBAVCODEC_VERSION_INT < AV_VERSION_INT(60, 31, 102)
- const bool mastering_display_metadata_added = av_stream_add_side_data(video_stream, AV_PKT_DATA_MASTERING_DISPLAY_METADATA, (uint8_t*)mastering_display_metadata, mastering_display_metadata_size) == 0;
- #else
- const bool mastering_display_metadata_added = av_packet_side_data_add(&video_stream->codecpar->coded_side_data, &video_stream->codecpar->nb_coded_side_data, AV_PKT_DATA_MASTERING_DISPLAY_METADATA, mastering_display_metadata, mastering_display_metadata_size, 0) != NULL;
- #endif
-
- if(!content_light_level_added)
- av_freep(&light_metadata);
-
- if(!mastering_display_metadata_added)
- av_freep(&mastering_display_metadata);
-
- // Return true even on failure because we dont want to retry adding hdr metadata on failure
- return true;
-}
-
-static void set_format_context_options(AVFormatContext *av_format_context) {
- if(LIBAVFORMAT_VERSION_INT >= AV_VERSION_INT(62, 6, 101)) {
- av_opt_set(av_format_context->priv_data, "use_editlist", "1", 0);
- const AVOption *opt = av_opt_find(av_format_context->priv_data, "movflags", NULL, 0, 0);
- if (opt && opt->unit) {
- const AVOption *flag = av_opt_find(av_format_context->priv_data, "hybrid_fragmented", opt->unit, 0, 0);
- if (flag)
- av_opt_set(av_format_context->priv_data, "movflags", "+hybrid_fragmented", 0);
- }
- } else {
- const AVOutputFormat *output_format = av_format_context->oformat;
- const char *file_extension = output_format->extensions ? output_format->extensions : "";
- if(strcmp(file_extension, "mp4") != 0 && strcmp(file_extension, "mov") != 0)
- return;
-
- gsr_log(GSR_LOG_LEVEL_WARNING, "your FFmpeg version is known to be buggy (it doesn't have working hybrid_fragmented movflags). If you experience stutter in the mp4 file then update your FFmpeg to at least version 8 or record to a mkv file");
- }
-}
-
-struct RecordingStartAudio {
- const gsr_audio_track *audio_track;
- AVStream *stream;
-};
-
-struct RecordingStartResult {
- AVFormatContext *av_format_context = nullptr;
- AVStream *video_stream = nullptr;
- std::vector<RecordingStartAudio> audio_inputs;
-};
-
-static RecordingStartResult start_recording_create_streams(const char *filename, const args_parser &arg_parser, AVCodecContext *video_codec_context, const gsr_audio_capture *audio_capture, bool hdr, gsr_video_sources *video_sources) {
- AVFormatContext *av_format_context;
- avformat_alloc_output_context2(&av_format_context, nullptr, arg_parser.settings.container_format, filename);
- set_format_context_options(av_format_context);
-
- AVStream *video_stream = create_stream(av_format_context, video_codec_context);
- avcodec_parameters_from_context(video_stream->codecpar, video_codec_context);
-
- RecordingStartResult result;
- result.audio_inputs.reserve(audio_capture->num_tracks);
-
- for(size_t audio_track_index = 0; audio_track_index < audio_capture->num_tracks; ++audio_track_index) {
- const gsr_audio_track &audio_track = audio_capture->tracks[audio_track_index];
- AVStream *audio_stream = create_stream(av_format_context, audio_track.codec_context);
- if(!audio_track.name[0] == '\0' && !arg_parser.settings.exclude_metadata)
- av_dict_set(&audio_stream->metadata, "title", audio_track.name, 0);
- avcodec_parameters_from_context(audio_stream->codecpar, audio_track.codec_context);
- result.audio_inputs.push_back({&audio_track, audio_stream});
- }
-
- const int open_ret = avio_open(&av_format_context->pb, filename, AVIO_FLAG_WRITE);
- if(open_ret < 0) {
- gsr_log(GSR_LOG_LEVEL_ERROR, "start_recording_create_streams: could not open '%s': %s", filename, gsr_av_error_to_string(open_ret));
- return result;
- }
-
- AVDictionary *options = nullptr;
- av_dict_set(&options, "strict", "experimental", 0);
-
- if(arg_parser.settings.ffmpeg_opts)
- av_dict_parse_string(&options, arg_parser.settings.ffmpeg_opts, "=", ";", 0);
-
- const int header_write_ret = avformat_write_header(av_format_context, &options);
- av_dict_free(&options);
- if(header_write_ret < 0) {
- gsr_log(GSR_LOG_LEVEL_ERROR, "start_recording_create_streams: error occurred when writing header to output file: %s", gsr_av_error_to_string(header_write_ret));
- avio_close(av_format_context->pb);
- avformat_free_context(av_format_context);
- return result;
- }
-
- for(size_t video_source_index = 0; video_source_index < video_sources->num_items; ++video_source_index) {
- gsr_video_source &video_source = video_sources->items[video_source_index];
- if(hdr && add_hdr_metadata_to_video_stream(video_source.capture, video_stream))
- break;
- }
-
- result.av_format_context = av_format_context;
- result.video_stream = video_stream;
- return result;
-}
-
-static bool stop_recording_close_streams(AVFormatContext *av_format_context) {
- bool trailer_written = true;
- if(av_write_trailer(av_format_context) != 0) {
- //fprintf(stderr, "gsr error: end: failed to write trailer\n");
- //trailer_written = false;
- }
-
- const bool closed = avio_close(av_format_context->pb) == 0;
- avformat_free_context(av_format_context);
- return trailer_written && closed;
-}
-
-static std::future<bool> save_replay_thread;
-static std::string save_replay_output_filepath;
-
-static std::string create_new_recording_filepath_from_timestamp(std::string directory, const char *filename_prefix, const std::string &file_extension, bool date_folders) {
- char date_str[128];
- std::string output_filepath;
- if(date_folders) {
- gsr_get_date_only_str(date_str, sizeof(date_str));
- std::string output_folder = directory + '/' + date_str;
- if(create_directory_recursive(&output_folder[0]) != 0)
- gsr_log(GSR_LOG_LEVEL_ERROR, "failed to create directory: %s", output_folder.c_str());
- gsr_get_time_only_str(date_str, sizeof(date_str));
- output_filepath = output_folder + "/" + filename_prefix + "_" + date_str + "." + file_extension;
- } else {
- if(create_directory_recursive(&directory[0]) != 0)
- gsr_log(GSR_LOG_LEVEL_ERROR, "failed to create directory: %s", directory.c_str());
- gsr_get_date_str(date_str, sizeof(date_str));
- output_filepath = directory + "/" + filename_prefix + "_" + date_str + "." + file_extension;
- }
- return output_filepath;
-}
-
-static RecordingStartAudio* get_recording_start_item_by_stream_index(RecordingStartResult &result, int stream_index) {
- for(auto &audio_input : result.audio_inputs) {
- if(audio_input.stream->index == stream_index)
- return &audio_input;
- }
- return nullptr;
-}
-
-struct AudioPtsOffset {
- int64_t pts_offset = 0;
- int stream_index = 0;
-};
-
-static bool save_replay_async(AVCodecContext *video_codec_context, int video_stream_index, const gsr_audio_capture *audio_capture, gsr_encoder *encoder, const args_parser &arg_parser, const std::string &file_extension, bool date_folders, bool hdr, gsr_video_sources *video_sources, int current_save_replay_seconds) {
- if(save_replay_thread.valid())
- return true;
-
- pthread_mutex_lock(&encoder->replay_mutex);
- gsr_replay_buffer *cloned_replay_buffer = gsr_replay_buffer_clone(encoder->replay_buffer);
- pthread_mutex_unlock(&encoder->replay_mutex);
- if(!cloned_replay_buffer) {
- // TODO: Return this error to mark the replay as failed
- gsr_log(GSR_LOG_LEVEL_ERROR, "failed to save replay: failed to clone replay buffer");
- return false;
- }
-
- const gsr_replay_buffer_iterator search_start_iterator = current_save_replay_seconds == save_replay_seconds_full ? gsr_replay_buffer_iterator{0, 0} : gsr_replay_buffer_find_packet_index_by_time_passed(cloned_replay_buffer, current_save_replay_seconds);
- const gsr_replay_buffer_iterator video_start_iterator = gsr_replay_buffer_find_keyframe(cloned_replay_buffer, search_start_iterator, video_stream_index, false);
- if(video_start_iterator.packet_index == (size_t)-1) {
- gsr_log(GSR_LOG_LEVEL_ERROR, "failed to save replay: failed to find a video keyframe. perhaps replay was saved too fast, before anything has been recorded");
- pthread_mutex_lock(&encoder->replay_mutex);
- gsr_replay_buffer_destroy(cloned_replay_buffer);
- pthread_mutex_unlock(&encoder->replay_mutex);
- return true;
- }
-
- const int64_t video_pts_offset = gsr_replay_buffer_iterator_get_packet(cloned_replay_buffer, video_start_iterator)->pts;
-
- std::vector<AudioPtsOffset> audio_pts_offsets;
- audio_pts_offsets.reserve(audio_capture->num_tracks);
- for(size_t audio_track_index = 0; audio_track_index < audio_capture->num_tracks; ++audio_track_index) {
- const gsr_audio_track &audio_track = audio_capture->tracks[audio_track_index];
- const gsr_replay_buffer_iterator audio_start_iterator = gsr_replay_buffer_find_keyframe(cloned_replay_buffer, video_start_iterator, audio_track.stream_index, false);
- const int64_t audio_pts_offset = audio_start_iterator.packet_index == (size_t)-1 ? 0 : gsr_replay_buffer_iterator_get_packet(cloned_replay_buffer, audio_start_iterator)->pts;
- audio_pts_offsets.push_back(AudioPtsOffset{audio_pts_offset, audio_track.stream_index});
- }
-
- std::string output_filepath = create_new_recording_filepath_from_timestamp(arg_parser.settings.filename, "Replay", file_extension, date_folders);
- RecordingStartResult recording_start_result = start_recording_create_streams(output_filepath.c_str(), arg_parser, video_codec_context, audio_capture, hdr, video_sources);
- if(!recording_start_result.av_format_context) {
- pthread_mutex_lock(&encoder->replay_mutex);
- gsr_replay_buffer_destroy(cloned_replay_buffer);
- pthread_mutex_unlock(&encoder->replay_mutex);
- return false;
- }
-
- save_replay_output_filepath = std::move(output_filepath);
-
- save_replay_thread = std::async(std::launch::async, [video_stream_index, recording_start_result, video_start_iterator, video_pts_offset, audio_pts_offsets{std::move(audio_pts_offsets)}, video_codec_context, cloned_replay_buffer, encoder]() mutable {
- bool success = true;
- gsr_replay_buffer_iterator replay_iterator = video_start_iterator;
-
- for(;;) {
- AVPacket *replay_packet = gsr_replay_buffer_iterator_get_packet(cloned_replay_buffer, replay_iterator);
- uint8_t *replay_packet_data = NULL;
- if(replay_packet) {
- pthread_mutex_lock(&encoder->replay_mutex);
- replay_packet_data = gsr_replay_buffer_iterator_get_packet_data(cloned_replay_buffer, replay_iterator);
- pthread_mutex_unlock(&encoder->replay_mutex);
- }
-
- if(!replay_packet) {
- gsr_log(GSR_LOG_LEVEL_ERROR, "save_replay_async: no replay packet");
- success = false;
- break;
- }
-
- if(!replay_packet->data && !replay_packet_data) {
- gsr_log(GSR_LOG_LEVEL_ERROR, "save_replay_async: no replay packet data");
- success = false;
- break;
- }
-
- // TODO: Check if successful
- AVPacket av_packet;
- memset(&av_packet, 0, sizeof(av_packet));
- //av_packet_from_data(av_packet, replay_packet->data, replay_packet->size);
- av_packet.data = replay_packet->data ? replay_packet->data : replay_packet_data;
- av_packet.size = replay_packet->size;
- av_packet.stream_index = replay_packet->stream_index;
- av_packet.pts = replay_packet->pts;
- av_packet.dts = replay_packet->pts;
- av_packet.flags = replay_packet->flags;
- //av_packet.duration = replay_packet->duration;
-
- AVStream *stream = recording_start_result.video_stream;
- AVCodecContext *codec_context = video_codec_context;
-
- if(av_packet.stream_index == video_stream_index) {
- av_packet.pts -= video_pts_offset;
- av_packet.dts -= video_pts_offset;
- } else {
- RecordingStartAudio *recording_start_audio = get_recording_start_item_by_stream_index(recording_start_result, av_packet.stream_index);
- if(!recording_start_audio) {
- gsr_log(GSR_LOG_LEVEL_ERROR, "save_replay_async: failed to find audio stream by index: %d", av_packet.stream_index);
- free(replay_packet_data);
- continue;
- }
-
- const gsr_audio_track *audio_track = recording_start_audio->audio_track;
- stream = recording_start_audio->stream;
- codec_context = audio_track->codec_context;
-
- const AudioPtsOffset &audio_pts_offset = audio_pts_offsets[av_packet.stream_index - 1];
- assert(audio_pts_offset.stream_index == av_packet.stream_index);
- av_packet.pts -= audio_pts_offset.pts_offset;
- av_packet.dts -= audio_pts_offset.pts_offset;
- }
-
- //av_packet.stream_index = stream->index;
- av_packet_rescale_ts(&av_packet, codec_context->time_base, stream->time_base);
-
- const int ret = av_write_frame(recording_start_result.av_format_context, &av_packet);
- if(ret < 0)
- gsr_log(GSR_LOG_LEVEL_ERROR, "Failed to write frame index %d to muxer, reason: %s (%d)", av_packet.stream_index, gsr_av_error_to_string(ret), ret);
-
- free(replay_packet_data);
-
- //av_packet_free(&av_packet);
- if(!gsr_replay_buffer_iterator_next(cloned_replay_buffer, &replay_iterator))
- break;
- }
-
- stop_recording_close_streams(recording_start_result.av_format_context);
-
- pthread_mutex_lock(&encoder->replay_mutex);
- gsr_replay_buffer_destroy(cloned_replay_buffer);
- pthread_mutex_unlock(&encoder->replay_mutex);
-
- return success;
- });
-
- return true;
-}
-
static void list_system_info(bool wayland) {
printf("display_server|%s\n", wayland ? "wayland" : "x11");
bool supports_app_audio = false;
@@ -1014,31 +689,6 @@ static bool get_image_format_from_filename(const char *filename, gsr_image_forma
}
}
-static void av_write_header(AVFormatContext *av_format_context, const char *ffmpeg_opts) {
- AVDictionary *options = nullptr;
- av_dict_set(&options, "strict", "experimental", 0);
-
- if(ffmpeg_opts)
- av_dict_parse_string(&options, ffmpeg_opts, "=", ";", 0);
-
- const int ret = avformat_write_header(av_format_context, &options);
- if(ret < 0)
- fprintf(stderr, "Error occurred when writing header to output file: %s\n", gsr_av_error_to_string(ret));
-
- av_dict_free(&options);
-}
-
-static size_t calculate_estimated_replay_buffer_packets(int64_t replay_buffer_size_secs, int fps, gsr_audio_codec audio_codec, const gsr_audio_input_tracks *audio_inputs) {
- if(replay_buffer_size_secs == -1)
- return 0;
-
- int audio_fps = 0;
- if(audio_inputs->num_items > 0)
- audio_fps = GSR_AUDIO_SAMPLE_RATE / audio_codec_get_frame_size(audio_codec);
-
- return replay_buffer_size_secs * (fps + audio_fps * audio_inputs->num_items);
-}
-
static void set_display_server_environment_variables() {
// Some users dont have properly setup environments (no display manager that does systemctl --user import-environment DISPLAY WAYLAND_DISPLAY)
const char *display = getenv("DISPLAY");
@@ -1584,7 +1234,10 @@ int main(int argc, char **argv) {
bool paused = false;
bool replay_recording = false;
- RecordingStartResult replay_recording_start_result;
+ gsr_recording_output replay_recording_output;
+ memset(&replay_recording_output, 0, sizeof(replay_recording_output));
+ gsr_replay_save replay_save;
+ gsr_replay_save_init(&replay_save);
std::vector<size_t> replay_recording_items;
std::string replay_recording_filepath;
bool force_iframe_frame = false; // Only needed for video since audio frames are always iframes
@@ -1828,10 +1481,11 @@ int main(int argc, char **argv) {
if(new_replay_recording_state) {
gsr_audio_capture_lock_filter(&audio_capture);
replay_recording_items.clear();
- replay_recording_filepath = create_new_recording_filepath_from_timestamp(arg_parser.settings.replay_recording_directory, "Video", file_extension, arg_parser.settings.date_folders);
- replay_recording_start_result = start_recording_create_streams(replay_recording_filepath.c_str(), arg_parser, video_codec_context, &audio_capture, hdr, video_sources);
- if(replay_recording_start_result.av_format_context) {
- const size_t video_recording_destination_id = gsr_encoder_add_recording_destination(&encoder, video_codec_context, replay_recording_start_result.av_format_context, replay_recording_start_result.video_stream, video_frame->pts);
+ char replay_recording_filepath_buf[PATH_MAX];
+ const bool filepath_created = gsr_create_new_recording_filepath_from_timestamp(replay_recording_filepath_buf, sizeof(replay_recording_filepath_buf), arg_parser.settings.replay_recording_directory, "Video", file_extension.c_str(), arg_parser.settings.date_folders);
+ replay_recording_filepath = filepath_created ? replay_recording_filepath_buf : "";
+ if(filepath_created && gsr_recording_output_start(&replay_recording_output, replay_recording_filepath.c_str(), &arg_parser.settings, video_codec_context, &audio_capture, hdr, video_sources)) {
+ const size_t video_recording_destination_id = gsr_encoder_add_recording_destination(&encoder, video_codec_context, replay_recording_output.av_format_context, replay_recording_output.video_stream, video_frame->pts);
if(arg_parser.settings.write_first_frame_ts && video_recording_destination_id != (size_t)-1) {
std::string ts_filepath = replay_recording_filepath + ".ts";
gsr_encoder_set_recording_destination_first_frame_ts_filepath(&encoder, video_recording_destination_id, ts_filepath.c_str());
@@ -1840,27 +1494,29 @@ int main(int argc, char **argv) {
if(video_recording_destination_id != (size_t)-1)
replay_recording_items.push_back(video_recording_destination_id);
- for(const auto &audio_input : replay_recording_start_result.audio_inputs) {
- const size_t audio_recording_destination_id = gsr_encoder_add_recording_destination(&encoder, audio_input.audio_track->codec_context, replay_recording_start_result.av_format_context, audio_input.stream, audio_input.audio_track->pts);
+ for(size_t i = 0; i < replay_recording_output.num_audio_streams; ++i) {
+ const gsr_recording_audio_stream *audio_stream = &replay_recording_output.audio_streams[i];
+ const size_t audio_recording_destination_id = gsr_encoder_add_recording_destination(&encoder, audio_stream->audio_track->codec_context, replay_recording_output.av_format_context, audio_stream->stream, audio_stream->audio_track->pts);
if(audio_recording_destination_id != (size_t)-1)
replay_recording_items.push_back(audio_recording_destination_id);
}
replay_recording = true;
force_iframe_frame = true;
- fprintf(stderr, "Started recording\n");
+ gsr_log(GSR_LOG_LEVEL_INFO, "Started recording");
} else {
printf("gsr error: Failed to start recording\n");
fflush(stdout);
}
- } else if(replay_recording_start_result.av_format_context) {
+ gsr_audio_capture_unlock_filter(&audio_capture);
+ } else if(replay_recording_output.av_format_context) {
for(size_t id : replay_recording_items) {
gsr_encoder_remove_recording_destination(&encoder, id);
}
replay_recording_items.clear();
- if(stop_recording_close_streams(replay_recording_start_result.av_format_context)) {
- fprintf(stderr, "Stopped recording\n");
+ if(gsr_recording_output_stop(&replay_recording_output)) {
+ gsr_log(GSR_LOG_LEVEL_INFO, "Stopped recording");
puts(replay_recording_filepath.c_str());
fflush(stdout);
if(arg_parser.settings.recording_saved_script)
@@ -1870,39 +1526,38 @@ int main(int argc, char **argv) {
fflush(stdout);
}
- replay_recording_start_result = RecordingStartResult{};
replay_recording = false;
replay_recording_filepath.clear();
}
}
- if(save_replay_thread.valid() && save_replay_thread.wait_for(std::chrono::seconds(0)) == std::future_status::ready) {
- const bool replay_save_result = save_replay_thread.get();
- if(save_replay_output_filepath.empty() || !replay_save_result) {
+ bool replay_save_result = false;
+ const char *replay_save_output_filepath = nullptr;
+ if(gsr_replay_save_poll(&replay_save, &replay_save_result, &replay_save_output_filepath)) {
+ if(replay_save_output_filepath[0] == '\0' || !replay_save_result) {
printf("gsr error: Failed to save replay\n");
fflush(stdout);
} else {
- puts(save_replay_output_filepath.c_str());
+ puts(replay_save_output_filepath);
fflush(stdout);
if(arg_parser.settings.recording_saved_script)
- run_recording_saved_script_async(arg_parser.settings.recording_saved_script, save_replay_output_filepath.c_str(), "replay");
+ run_recording_saved_script_async(arg_parser.settings.recording_saved_script, replay_save_output_filepath, "replay");
}
}
- if(save_replay_seconds != 0 && !save_replay_thread.valid() && arg_parser.settings.is_replaying) {
+ if(save_replay_seconds != 0 && !gsr_replay_save_is_running(&replay_save) && arg_parser.settings.is_replaying) {
int current_save_replay_seconds = save_replay_seconds;
if(current_save_replay_seconds > 0)
current_save_replay_seconds += arg_parser.settings.keyint;
save_replay_seconds = 0;
- save_replay_output_filepath.clear();
- const bool replay_start_result = save_replay_async(video_codec_context, VIDEO_STREAM_INDEX, &audio_capture, &encoder, arg_parser, file_extension, arg_parser.settings.date_folders, hdr, video_sources, current_save_replay_seconds);
+ const bool replay_start_result = gsr_replay_save_start(&replay_save, video_codec_context, VIDEO_STREAM_INDEX, &audio_capture, &encoder, &arg_parser.settings, file_extension.c_str(), hdr, video_sources, current_save_replay_seconds);
if(!replay_start_result) {
printf("gsr error: Failed to save replay\n");
fflush(stdout);
}
- if(arg_parser.settings.restart_replay_on_save && current_save_replay_seconds == save_replay_seconds_full) {
+ if(arg_parser.settings.restart_replay_on_save && current_save_replay_seconds == GSR_SAVE_REPLAY_SECONDS_FULL) {
pthread_mutex_lock(&encoder.replay_mutex);
gsr_replay_buffer_clear(encoder.replay_buffer);
pthread_mutex_unlock(&encoder.replay_mutex);
@@ -1930,28 +1585,29 @@ int main(int argc, char **argv) {
running = 0;
- if(save_replay_thread.valid()) {
- save_replay_thread.get();
- if(save_replay_output_filepath.empty()) {
+ bool final_replay_save_result = false;
+ const char *final_replay_save_output_filepath = nullptr;
+ if(gsr_replay_save_join(&replay_save, &final_replay_save_result, &final_replay_save_output_filepath)) {
+ if(final_replay_save_output_filepath[0] == '\0') {
// TODO: Output failed to save
} else {
- puts(save_replay_output_filepath.c_str());
+ puts(final_replay_save_output_filepath);
fflush(stdout);
if(arg_parser.settings.recording_saved_script)
- run_recording_saved_script_async(arg_parser.settings.recording_saved_script, save_replay_output_filepath.c_str(), "replay");
+ run_recording_saved_script_async(arg_parser.settings.recording_saved_script, final_replay_save_output_filepath, "replay");
}
}
gsr_plugins_deinit(&plugins);
- if(replay_recording_start_result.av_format_context) {
+ if(replay_recording_output.av_format_context) {
for(size_t id : replay_recording_items) {
gsr_encoder_remove_recording_destination(&encoder, id);
}
replay_recording_items.clear();
- if(stop_recording_close_streams(replay_recording_start_result.av_format_context)) {
- fprintf(stderr, "Stopped recording\n");
+ if(gsr_recording_output_stop(&replay_recording_output)) {
+ gsr_log(GSR_LOG_LEVEL_INFO, "Stopped recording");
puts(replay_recording_filepath.c_str());
fflush(stdout);
if(arg_parser.settings.recording_saved_script)
diff --git a/src/recorder/muxer.c b/src/recorder/muxer.c
new file mode 100644
index 0000000..ff4ec06
--- /dev/null
+++ b/src/recorder/muxer.c
@@ -0,0 +1,268 @@
+#include "../../include/recorder/muxer.h"
+#include "../../include/recorder/audio_codec.h"
+#include "../../include/ffmpeg_utils.h"
+#include "../../include/utils.h"
+#include "../../include/log.h"
+
+#include <string.h>
+#include <stdlib.h>
+#include <stdio.h>
+
+#include <libavutil/opt.h>
+#include <libavutil/mastering_display_metadata.h>
+
+static void gsr_recording_output_deinit(gsr_recording_output *self) {
+ if(self->audio_streams) {
+ free(self->audio_streams);
+ self->audio_streams = NULL;
+ }
+ self->num_audio_streams = 0;
+}
+
+AVStream* create_stream(AVFormatContext *av_format_context, AVCodecContext *codec_context) {
+ AVStream *stream = avformat_new_stream(av_format_context, NULL);
+ if (!stream) {
+ gsr_log(GSR_LOG_LEVEL_ERROR, "Could not allocate stream");
+ return NULL;
+ }
+ stream->id = av_format_context->nb_streams - 1;
+ stream->time_base = codec_context->time_base;
+ stream->avg_frame_rate = codec_context->framerate;
+ //stream->r_frame_rate = codec_context->framerate;
+ return stream;
+}
+
+bool add_hdr_metadata_to_video_stream(gsr_capture *cap, AVStream *video_stream) {
+ size_t light_metadata_size = 0;
+ size_t mastering_display_metadata_size = 0;
+ AVContentLightMetadata *light_metadata = av_content_light_metadata_alloc(&light_metadata_size);
+ #if LIBAVUTIL_VERSION_INT < AV_VERSION_INT(59, 37, 100)
+ AVMasteringDisplayMetadata *mastering_display_metadata = av_mastering_display_metadata_alloc();
+ mastering_display_metadata_size = sizeof(*mastering_display_metadata);
+ #else
+ AVMasteringDisplayMetadata *mastering_display_metadata = av_mastering_display_metadata_alloc_size(&mastering_display_metadata_size);
+ #endif
+
+ if(!light_metadata || !mastering_display_metadata) {
+ if(light_metadata)
+ av_freep(&light_metadata);
+
+ if(mastering_display_metadata)
+ av_freep(&mastering_display_metadata);
+
+ return false;
+ }
+
+ if(!gsr_capture_set_hdr_metadata(cap, mastering_display_metadata, light_metadata)) {
+ av_freep(&light_metadata);
+ av_freep(&mastering_display_metadata);
+ return false;
+ }
+
+ // TODO: More error checking
+
+ #if LIBAVCODEC_VERSION_INT < AV_VERSION_INT(60, 31, 102)
+ const bool content_light_level_added = av_stream_add_side_data(video_stream, AV_PKT_DATA_CONTENT_LIGHT_LEVEL, (uint8_t*)light_metadata, light_metadata_size) == 0;
+ #else
+ const bool content_light_level_added = av_packet_side_data_add(&video_stream->codecpar->coded_side_data, &video_stream->codecpar->nb_coded_side_data, AV_PKT_DATA_CONTENT_LIGHT_LEVEL, light_metadata, light_metadata_size, 0) != NULL;
+ #endif
+
+ #if LIBAVCODEC_VERSION_INT < AV_VERSION_INT(60, 31, 102)
+ const bool mastering_display_metadata_added = av_stream_add_side_data(video_stream, AV_PKT_DATA_MASTERING_DISPLAY_METADATA, (uint8_t*)mastering_display_metadata, mastering_display_metadata_size) == 0;
+ #else
+ const bool mastering_display_metadata_added = av_packet_side_data_add(&video_stream->codecpar->coded_side_data, &video_stream->codecpar->nb_coded_side_data, AV_PKT_DATA_MASTERING_DISPLAY_METADATA, mastering_display_metadata, mastering_display_metadata_size, 0) != NULL;
+ #endif
+
+ if(!content_light_level_added)
+ av_freep(&light_metadata);
+
+ if(!mastering_display_metadata_added)
+ av_freep(&mastering_display_metadata);
+
+ // Return true even on failure because we dont want to retry adding hdr metadata on failure
+ return true;
+}
+
+void set_format_context_options(AVFormatContext *av_format_context) {
+ if(LIBAVFORMAT_VERSION_INT >= AV_VERSION_INT(62, 6, 101)) {
+ av_opt_set(av_format_context->priv_data, "use_editlist", "1", 0);
+ const AVOption *opt = av_opt_find(av_format_context->priv_data, "movflags", NULL, 0, 0);
+ if (opt && opt->unit) {
+ const AVOption *flag = av_opt_find(av_format_context->priv_data, "hybrid_fragmented", opt->unit, 0, 0);
+ if (flag)
+ av_opt_set(av_format_context->priv_data, "movflags", "+hybrid_fragmented", 0);
+ }
+ } else {
+ const AVOutputFormat *output_format = av_format_context->oformat;
+ const char *file_extension = output_format->extensions ? output_format->extensions : "";
+ if(strcmp(file_extension, "mp4") != 0 && strcmp(file_extension, "mov") != 0)
+ return;
+
+ gsr_log(GSR_LOG_LEVEL_WARNING, "your FFmpeg version is known to be buggy (it doesn't have working hybrid_fragmented movflags). If you experience stutter in the mp4 file then update your FFmpeg to at least version 8 or record to a mkv file");
+ }
+}
+
+void av_write_header(AVFormatContext *av_format_context, const char *ffmpeg_opts) {
+ AVDictionary *options = NULL;
+ av_dict_set(&options, "strict", "experimental", 0);
+
+ if(ffmpeg_opts)
+ av_dict_parse_string(&options, ffmpeg_opts, "=", ";", 0);
+
+ const int ret = avformat_write_header(av_format_context, &options);
+ if(ret < 0)
+ gsr_log(GSR_LOG_LEVEL_ERROR, "error occurred when writing header to output file: %s", gsr_av_error_to_string(ret));
+
+ av_dict_free(&options);
+}
+
+bool gsr_recording_output_start(gsr_recording_output *self, const char *filename, const gsr_recorder_settings *settings, AVCodecContext *video_codec_context, const gsr_audio_capture *audio_capture, bool hdr, gsr_video_sources *video_sources) {
+ memset(self, 0, sizeof(*self));
+
+ AVFormatContext *av_format_context = NULL;
+ avformat_alloc_output_context2(&av_format_context, NULL, settings->container_format, filename);
+ if(!av_format_context) {
+ gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_recording_output_start: failed to create output context for '%s'", filename);
+ return false;
+ }
+ set_format_context_options(av_format_context);
+
+ AVStream *video_stream = create_stream(av_format_context, video_codec_context);
+ if(!video_stream) {
+ avformat_free_context(av_format_context);
+ return false;
+ }
+ avcodec_parameters_from_context(video_stream->codecpar, video_codec_context);
+
+ if(audio_capture->num_tracks > 0) {
+ self->audio_streams = calloc(audio_capture->num_tracks, sizeof(gsr_recording_audio_stream));
+ if(!self->audio_streams) {
+ gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_recording_output_start: failed to allocate audio streams");
+ avformat_free_context(av_format_context);
+ return false;
+ }
+ }
+
+ for(size_t i = 0; i < audio_capture->num_tracks; ++i) {
+ const gsr_audio_track *audio_track = &audio_capture->tracks[i];
+ AVStream *audio_stream = create_stream(av_format_context, audio_track->codec_context);
+ if(!audio_stream) {
+ gsr_recording_output_deinit(self);
+ avformat_free_context(av_format_context);
+ return false;
+ }
+
+ if(audio_track->name[0] != '\0' && !settings->exclude_metadata)
+ av_dict_set(&audio_stream->metadata, "title", audio_track->name, 0);
+ avcodec_parameters_from_context(audio_stream->codecpar, audio_track->codec_context);
+
+ self->audio_streams[i].audio_track = audio_track;
+ self->audio_streams[i].stream = audio_stream;
+ ++self->num_audio_streams;
+ }
+
+ const int open_ret = avio_open(&av_format_context->pb, filename, AVIO_FLAG_WRITE);
+ if(open_ret < 0) {
+ gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_recording_output_start: could not open '%s': %s", filename, gsr_av_error_to_string(open_ret));
+ gsr_recording_output_deinit(self);
+ avformat_free_context(av_format_context);
+ return false;
+ }
+
+ AVDictionary *options = NULL;
+ av_dict_set(&options, "strict", "experimental", 0);
+
+ if(settings->ffmpeg_opts)
+ av_dict_parse_string(&options, settings->ffmpeg_opts, "=", ";", 0);
+
+ const int header_write_ret = avformat_write_header(av_format_context, &options);
+ av_dict_free(&options);
+ if(header_write_ret < 0) {
+ gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_recording_output_start: error occurred when writing header to output file: %s", gsr_av_error_to_string(header_write_ret));
+ avio_close(av_format_context->pb);
+ gsr_recording_output_deinit(self);
+ avformat_free_context(av_format_context);
+ return false;
+ }
+
+ for(size_t i = 0; i < video_sources->num_items; ++i) {
+ if(hdr && add_hdr_metadata_to_video_stream(video_sources->items[i].capture, video_stream))
+ break;
+ }
+
+ self->av_format_context = av_format_context;
+ self->video_stream = video_stream;
+ return true;
+}
+
+bool gsr_recording_output_stop(gsr_recording_output *self) {
+ bool trailer_written = true;
+ if(av_write_trailer(self->av_format_context) != 0) {
+ //trailer_written = false;
+ }
+
+ const bool closed = avio_close(self->av_format_context->pb) == 0;
+ avformat_free_context(self->av_format_context);
+ self->av_format_context = NULL;
+ self->video_stream = NULL;
+ gsr_recording_output_deinit(self);
+ return trailer_written && closed;
+}
+
+bool gsr_create_new_recording_filepath_from_timestamp(char *filepath, size_t filepath_size, const char *directory, const char *filename_prefix, const char *file_extension, bool date_folders) {
+ char date_str[128];
+ char output_folder[PATH_MAX];
+ int written = 0;
+
+ if(date_folders) {
+ gsr_get_date_only_str(date_str, sizeof(date_str));
+ written = snprintf(output_folder, sizeof(output_folder), "%s/%s", directory, date_str);
+ if(written < 0 || written >= (int)sizeof(output_folder)) {
+ gsr_log(GSR_LOG_LEVEL_ERROR, "the directory path is too long: %s", directory);
+ return false;
+ }
+
+ if(create_directory_recursive(output_folder) != 0)
+ gsr_log(GSR_LOG_LEVEL_ERROR, "failed to create directory: %s", output_folder);
+
+ gsr_get_time_only_str(date_str, sizeof(date_str));
+ } else {
+ written = snprintf(output_folder, sizeof(output_folder), "%s", directory);
+ if(written < 0 || written >= (int)sizeof(output_folder)) {
+ gsr_log(GSR_LOG_LEVEL_ERROR, "the directory path is too long: %s", directory);
+ return false;
+ }
+
+ if(create_directory_recursive(output_folder) != 0)
+ gsr_log(GSR_LOG_LEVEL_ERROR, "failed to create directory: %s", output_folder);
+
+ gsr_get_date_str(date_str, sizeof(date_str));
+ }
+
+ written = snprintf(filepath, filepath_size, "%s/%s_%s.%s", output_folder, filename_prefix, date_str, file_extension);
+ if(written < 0 || written >= (int)filepath_size) {
+ gsr_log(GSR_LOG_LEVEL_ERROR, "the output filepath is too long");
+ return false;
+ }
+
+ return true;
+}
+
+gsr_recording_audio_stream* gsr_recording_output_get_audio_stream_by_index(gsr_recording_output *self, int stream_index) {
+ for(size_t i = 0; i < self->num_audio_streams; ++i) {
+ if(self->audio_streams[i].stream->index == stream_index)
+ return &self->audio_streams[i];
+ }
+ return NULL;
+}
+
+size_t calculate_estimated_replay_buffer_packets(int64_t replay_buffer_size_secs, int fps, gsr_audio_codec audio_codec, const gsr_audio_input_tracks *audio_inputs) {
+ if(replay_buffer_size_secs == -1)
+ return 0;
+
+ int audio_fps = 0;
+ if(audio_inputs->num_items > 0)
+ audio_fps = GSR_AUDIO_SAMPLE_RATE / audio_codec_get_frame_size(audio_codec);
+
+ return replay_buffer_size_secs * (fps + audio_fps * audio_inputs->num_items);
+}
diff --git a/src/recorder/replay_save.c b/src/recorder/replay_save.c
new file mode 100644
index 0000000..6e5bc7b
--- /dev/null
+++ b/src/recorder/replay_save.c
@@ -0,0 +1,207 @@
+#include "../../include/recorder/replay_save.h"
+#include "../../include/ffmpeg_utils.h"
+#include "../../include/log.h"
+
+#include <string.h>
+#include <stdlib.h>
+#include <assert.h>
+
+void gsr_replay_save_init(gsr_replay_save *self) {
+ memset(self, 0, sizeof(*self));
+ self->finished = 0;
+}
+
+bool gsr_replay_save_is_running(const gsr_replay_save *self) {
+ return self->thread_created;
+}
+
+static void gsr_replay_save_cleanup(gsr_replay_save *self) {
+ if(self->cloned_replay_buffer) {
+ pthread_mutex_lock(&self->encoder->replay_mutex);
+ gsr_replay_buffer_destroy(self->cloned_replay_buffer);
+ pthread_mutex_unlock(&self->encoder->replay_mutex);
+ self->cloned_replay_buffer = NULL;
+ }
+
+ if(self->audio_pts_offsets) {
+ free(self->audio_pts_offsets);
+ self->audio_pts_offsets = NULL;
+ }
+ self->num_audio_pts_offsets = 0;
+}
+
+static void* replay_save_thread(void *userdata) {
+ gsr_replay_save *self = userdata;
+ bool success = true;
+ gsr_replay_buffer_iterator replay_iterator = self->video_start_iterator;
+
+ for(;;) {
+ AVPacket *replay_packet = gsr_replay_buffer_iterator_get_packet(self->cloned_replay_buffer, replay_iterator);
+ uint8_t *replay_packet_data = NULL;
+ if(replay_packet) {
+ pthread_mutex_lock(&self->encoder->replay_mutex);
+ replay_packet_data = gsr_replay_buffer_iterator_get_packet_data(self->cloned_replay_buffer, replay_iterator);
+ pthread_mutex_unlock(&self->encoder->replay_mutex);
+ }
+
+ if(!replay_packet) {
+ gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_replay_save: no replay packet");
+ success = false;
+ break;
+ }
+
+ if(!replay_packet->data && !replay_packet_data) {
+ gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_replay_save: no replay packet data");
+ success = false;
+ break;
+ }
+
+ // TODO: Check if successful
+ AVPacket av_packet;
+ memset(&av_packet, 0, sizeof(av_packet));
+ //av_packet_from_data(av_packet, replay_packet->data, replay_packet->size);
+ av_packet.data = replay_packet->data ? replay_packet->data : replay_packet_data;
+ av_packet.size = replay_packet->size;
+ av_packet.stream_index = replay_packet->stream_index;
+ av_packet.pts = replay_packet->pts;
+ av_packet.dts = replay_packet->pts;
+ av_packet.flags = replay_packet->flags;
+ //av_packet.duration = replay_packet->duration;
+
+ AVStream *stream = self->recording_output.video_stream;
+ AVCodecContext *codec_context = self->video_codec_context;
+
+ if(av_packet.stream_index == self->video_stream_index) {
+ av_packet.pts -= self->video_pts_offset;
+ av_packet.dts -= self->video_pts_offset;
+ } else {
+ gsr_recording_audio_stream *recording_start_audio = gsr_recording_output_get_audio_stream_by_index(&self->recording_output, av_packet.stream_index);
+ if(!recording_start_audio) {
+ gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_replay_save: failed to find audio stream by index: %d", av_packet.stream_index);
+ free(replay_packet_data);
+ continue;
+ }
+
+ const gsr_audio_track *audio_track = recording_start_audio->audio_track;
+ stream = recording_start_audio->stream;
+ codec_context = audio_track->codec_context;
+
+ const gsr_audio_pts_offset *audio_pts_offset = &self->audio_pts_offsets[av_packet.stream_index - 1];
+ assert(audio_pts_offset->stream_index == av_packet.stream_index);
+ av_packet.pts -= audio_pts_offset->pts_offset;
+ av_packet.dts -= audio_pts_offset->pts_offset;
+ }
+
+ //av_packet.stream_index = stream->index;
+ av_packet_rescale_ts(&av_packet, codec_context->time_base, stream->time_base);
+
+ const int ret = av_write_frame(self->recording_output.av_format_context, &av_packet);
+ if(ret < 0)
+ gsr_log(GSR_LOG_LEVEL_ERROR, "Failed to write frame index %d to muxer, reason: %s (%d)", av_packet.stream_index, gsr_av_error_to_string(ret), ret);
+
+ free(replay_packet_data);
+
+ //av_packet_free(&av_packet);
+ if(!gsr_replay_buffer_iterator_next(self->cloned_replay_buffer, &replay_iterator))
+ break;
+ }
+
+ gsr_recording_output_stop(&self->recording_output);
+
+ self->success = success;
+ gsr_replay_save_cleanup(self);
+ self->finished = 1;
+ return NULL;
+}
+
+bool gsr_replay_save_start(gsr_replay_save *self, AVCodecContext *video_codec_context, int video_stream_index, const gsr_audio_capture *audio_capture, gsr_encoder *encoder, const gsr_recorder_settings *settings, const char *file_extension, bool hdr, gsr_video_sources *video_sources, int current_save_replay_seconds) {
+ if(self->thread_created)
+ return true;
+
+ self->encoder = encoder;
+ self->video_codec_context = video_codec_context;
+ self->video_stream_index = video_stream_index;
+ self->output_filepath[0] = '\0';
+ self->success = false;
+ self->finished = 0;
+
+ pthread_mutex_lock(&encoder->replay_mutex);
+ self->cloned_replay_buffer = gsr_replay_buffer_clone(encoder->replay_buffer);
+ pthread_mutex_unlock(&encoder->replay_mutex);
+ if(!self->cloned_replay_buffer) {
+ /* TODO: Return this error to mark the replay as failed */
+ gsr_log(GSR_LOG_LEVEL_ERROR, "failed to save replay: failed to clone replay buffer");
+ return false;
+ }
+
+ const gsr_replay_buffer_iterator start_iterator = {0, 0};
+ const gsr_replay_buffer_iterator search_start_iterator = current_save_replay_seconds == GSR_SAVE_REPLAY_SECONDS_FULL ? start_iterator : gsr_replay_buffer_find_packet_index_by_time_passed(self->cloned_replay_buffer, current_save_replay_seconds);
+ self->video_start_iterator = gsr_replay_buffer_find_keyframe(self->cloned_replay_buffer, search_start_iterator, video_stream_index, false);
+ if(self->video_start_iterator.packet_index == (size_t)-1) {
+ gsr_log(GSR_LOG_LEVEL_ERROR, "failed to save replay: failed to find a video keyframe. perhaps replay was saved too fast, before anything has been recorded");
+ gsr_replay_save_cleanup(self);
+ return true;
+ }
+
+ self->video_pts_offset = gsr_replay_buffer_iterator_get_packet(self->cloned_replay_buffer, self->video_start_iterator)->pts;
+
+ if(audio_capture->num_tracks > 0) {
+ self->audio_pts_offsets = calloc(audio_capture->num_tracks, sizeof(gsr_audio_pts_offset));
+ if(!self->audio_pts_offsets) {
+ gsr_log(GSR_LOG_LEVEL_ERROR, "failed to save replay: failed to allocate audio pts offsets");
+ gsr_replay_save_cleanup(self);
+ return false;
+ }
+ }
+
+ for(size_t i = 0; i < audio_capture->num_tracks; ++i) {
+ const gsr_audio_track *audio_track = &audio_capture->tracks[i];
+ const gsr_replay_buffer_iterator audio_start_iterator = gsr_replay_buffer_find_keyframe(self->cloned_replay_buffer, self->video_start_iterator, audio_track->stream_index, false);
+ const int64_t audio_pts_offset = audio_start_iterator.packet_index == (size_t)-1 ? 0 : gsr_replay_buffer_iterator_get_packet(self->cloned_replay_buffer, audio_start_iterator)->pts;
+ self->audio_pts_offsets[i].pts_offset = audio_pts_offset;
+ self->audio_pts_offsets[i].stream_index = audio_track->stream_index;
+ ++self->num_audio_pts_offsets;
+ }
+
+ if(!gsr_create_new_recording_filepath_from_timestamp(self->output_filepath, sizeof(self->output_filepath), settings->filename, "Replay", file_extension, settings->date_folders)) {
+ gsr_replay_save_cleanup(self);
+ return false;
+ }
+
+ if(!gsr_recording_output_start(&self->recording_output, self->output_filepath, settings, video_codec_context, audio_capture, hdr, video_sources)) {
+ gsr_replay_save_cleanup(self);
+ return false;
+ }
+
+ if(pthread_create(&self->thread, NULL, replay_save_thread, self) != 0) {
+ gsr_log(GSR_LOG_LEVEL_ERROR, "failed to save replay: failed to create thread");
+ gsr_recording_output_stop(&self->recording_output);
+ gsr_replay_save_cleanup(self);
+ return false;
+ }
+
+ self->thread_created = true;
+ return true;
+}
+
+static bool gsr_replay_save_finish(gsr_replay_save *self, bool *success, const char **output_filepath) {
+ pthread_join(self->thread, NULL);
+ self->thread_created = false;
+ *success = self->success;
+ *output_filepath = self->output_filepath;
+ return true;
+}
+
+bool gsr_replay_save_poll(gsr_replay_save *self, bool *success, const char **output_filepath) {
+ if(!self->thread_created || !self->finished)
+ return false;
+
+ return gsr_replay_save_finish(self, success, output_filepath);
+}
+
+bool gsr_replay_save_join(gsr_replay_save *self, bool *success, const char **output_filepath) {
+ if(!self->thread_created)
+ return false;
+
+ return gsr_replay_save_finish(self, success, output_filepath);
+}