Add -write-first-frame-ts switch

Add -write-first-frame-ts switch that creates a .ts file next to the output
file with values from CLOCK_MONOTONIC and CLOCK_REALTIME corresponding to the
first frame to be able to synchronize video with other timestamped data.
This commit is contained in:
Victor Nova
2026-01-27 10:27:36 -05:00
committed by dec05eba
parent f4ee71a094
commit 144b481526
6 changed files with 100 additions and 3 deletions

View File

@@ -196,7 +196,7 @@ static void usage_header(void) {
"[-bm auto|qp|vbr|cbr] [-cr limited|full] [-tune performance|quality] [-df yes|no] [-sc <script_path>] [-p <plugin_path>] "
"[-cursor yes|no] [-keyint <value>] [-restore-portal-session yes|no] [-portal-session-token-filepath filepath] [-encoder gpu|cpu] "
"[-fallback-cpu-encoding yes|no] [-o <output_file>] [-ro <output_directory>] [-ffmpeg-opts <options>] [--list-capture-options [card_path]] "
"[--list-audio-devices] [--list-application-audio] [--list-v4l2-devices] [-low-power yes|no] [-v yes|no] [-gl-debug yes|no] [--version] [-h|--help]\n", program_name);
"[--list-audio-devices] [--list-application-audio] [--list-v4l2-devices] [-write-first-frame-ts yes|no] [-low-power yes|no] [-v yes|no] [-gl-debug yes|no] [--version] [-h|--help]\n", program_name);
fflush(stdout);
}
@@ -255,6 +255,7 @@ static bool args_parser_set_values(args_parser *self) {
self->restart_replay_on_save = args_get_boolean_by_key(self->args, NUM_ARGS, "-restart-replay-on-save", false);
self->overclock = args_get_boolean_by_key(self->args, NUM_ARGS, "-oc", false);
self->fallback_cpu_encoding = args_get_boolean_by_key(self->args, NUM_ARGS, "-fallback-cpu-encoding", false);
self->write_first_frame_ts = args_get_boolean_by_key(self->args, NUM_ARGS, "-write-first-frame-ts", false);
self->low_power = args_get_boolean_by_key(self->args, NUM_ARGS, "-low-power", false);
self->audio_bitrate = args_get_i64_by_key(self->args, NUM_ARGS, "-ab", 0);
@@ -432,6 +433,10 @@ static bool args_parser_set_values(args_parser *self) {
self->is_output_piped = strcmp(self->filename, "/dev/stdout") == 0;
self->low_latency_recording = self->is_livestream || self->is_output_piped;
if(self->write_first_frame_ts && (self->is_livestream || self->is_output_piped)) {
fprintf(stderr, "gsr warning: -write-first-frame-ts is ignored for livestreaming or when output is piped\n");
self->write_first_frame_ts = false;
}
self->replay_recording_directory = args_get_value_by_key(self->args, NUM_ARGS, "-ro");
@@ -536,6 +541,7 @@ bool args_parser_parse(args_parser *self, int argc, char **argv, const args_hand
self->args[arg_index++] = (Arg){ .key = "-ffmpeg-opts", .optional = true, .list = false, .type = ARG_TYPE_STRING };
self->args[arg_index++] = (Arg){ .key = "-ffmpeg-video-opts", .optional = true, .list = false, .type = ARG_TYPE_STRING };
self->args[arg_index++] = (Arg){ .key = "-ffmpeg-audio-opts", .optional = true, .list = false, .type = ARG_TYPE_STRING };
self->args[arg_index++] = (Arg){ .key = "-write-first-frame-ts", .optional = true, .list = false, .type = ARG_TYPE_BOOLEAN };
self->args[arg_index++] = (Arg){ .key = "-low-power", .optional = true, .list = false, .type = ARG_TYPE_BOOLEAN };
assert(arg_index == NUM_ARGS);

View File

@@ -3,10 +3,37 @@
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <inttypes.h>
#include <time.h>
#include <errno.h>
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
static uint64_t clock_gettime_microseconds(clockid_t clock_id) {
struct timespec ts;
ts.tv_sec = 0;
ts.tv_nsec = 0;
clock_gettime(clock_id, &ts);
return (uint64_t)ts.tv_sec * 1000000ULL + (uint64_t)ts.tv_nsec / 1000ULL;
}
static void gsr_write_first_frame_timestamp_file(const char *filepath) {
const uint64_t evdev_compatible_ts = clock_gettime_microseconds(CLOCK_MONOTONIC);
const uint64_t unix_time_microsec = clock_gettime_microseconds(CLOCK_REALTIME);
FILE *file = fopen(filepath, "w");
if(!file) {
fprintf(stderr, "gsr warning: failed to open timestamp file '%s': %s\n", filepath, strerror(errno));
return;
}
fputs("monotonic_microsec\trealtime_microsec\n", file);
fprintf(file, "%" PRIu64 "\t%" PRIu64 "\n", evdev_compatible_ts, unix_time_microsec);
fclose(file);
}
bool gsr_encoder_init(gsr_encoder *self, gsr_replay_storage replay_storage, size_t replay_buffer_num_packets, double replay_buffer_time, const char *replay_directory) {
memset(self, 0, sizeof(*self));
self->num_recording_destinations = 0;
@@ -39,6 +66,16 @@ bool gsr_encoder_init(gsr_encoder *self, gsr_replay_storage replay_storage, size
}
void gsr_encoder_deinit(gsr_encoder *self) {
if(self->file_write_mutex_created)
pthread_mutex_lock(&self->file_write_mutex);
for(size_t i = 0; i < self->num_recording_destinations; ++i) {
free(self->recording_destinations[i].first_frame_ts_filepath);
self->recording_destinations[i].first_frame_ts_filepath = NULL;
self->recording_destinations[i].first_frame_ts_written = false;
}
if(self->file_write_mutex_created)
pthread_mutex_unlock(&self->file_write_mutex);
if(self->replay_buffer) {
pthread_mutex_lock(&self->replay_mutex);
gsr_replay_buffer_destroy(self->replay_buffer);
@@ -94,6 +131,11 @@ void gsr_encoder_receive_packets(gsr_encoder *self, AVCodecContext *codec_contex
else if(!recording_destination->has_received_keyframe)
continue;
if(recording_destination->first_frame_ts_filepath && !recording_destination->first_frame_ts_written) {
gsr_write_first_frame_timestamp_file(recording_destination->first_frame_ts_filepath);
recording_destination->first_frame_ts_written = true;
}
av_packet->pts = pts - recording_destination->start_pts;
av_packet->dts = pts - recording_destination->start_pts;
@@ -148,6 +190,8 @@ size_t gsr_encoder_add_recording_destination(gsr_encoder *self, AVCodecContext *
recording_destination->stream = stream;
recording_destination->start_pts = start_pts;
recording_destination->has_received_keyframe = false;
recording_destination->first_frame_ts_filepath = NULL;
recording_destination->first_frame_ts_written = false;
++self->recording_destination_id_counter;
++self->num_recording_destinations;
@@ -161,6 +205,9 @@ bool gsr_encoder_remove_recording_destination(gsr_encoder *self, size_t id) {
pthread_mutex_lock(&self->file_write_mutex);
for(size_t i = 0; i < self->num_recording_destinations; ++i) {
if(self->recording_destinations[i].id == id) {
free(self->recording_destinations[i].first_frame_ts_filepath);
self->recording_destinations[i].first_frame_ts_filepath = NULL;
self->recording_destinations[i].first_frame_ts_written = false;
self->recording_destinations[i] = self->recording_destinations[self->num_recording_destinations - 1];
--self->num_recording_destinations;
found = true;
@@ -170,3 +217,26 @@ bool gsr_encoder_remove_recording_destination(gsr_encoder *self, size_t id) {
pthread_mutex_unlock(&self->file_write_mutex);
return found;
}
bool gsr_encoder_set_recording_destination_first_frame_ts_filepath(gsr_encoder *self, size_t id, const char *filepath) {
if(!filepath)
return false;
bool found = false;
pthread_mutex_lock(&self->file_write_mutex);
for(size_t i = 0; i < self->num_recording_destinations; ++i) {
if(self->recording_destinations[i].id == id) {
char *filepath_copy = strdup(filepath);
if(!filepath_copy)
break;
free(self->recording_destinations[i].first_frame_ts_filepath);
self->recording_destinations[i].first_frame_ts_filepath = filepath_copy;
self->recording_destinations[i].first_frame_ts_written = false;
found = true;
break;
}
}
pthread_mutex_unlock(&self->file_write_mutex);
return found;
}

View File

@@ -3988,7 +3988,11 @@ int main(int argc, char **argv) {
if(video_stream) {
avcodec_parameters_from_context(video_stream->codecpar, video_codec_context);
gsr_encoder_add_recording_destination(&encoder, video_codec_context, av_format_context, video_stream, 0);
const size_t video_destination_id = gsr_encoder_add_recording_destination(&encoder, video_codec_context, av_format_context, video_stream, 0);
if(arg_parser.write_first_frame_ts && video_destination_id != (size_t)-1) {
std::string ts_filepath = std::string(arg_parser.filename) + ".ts";
gsr_encoder_set_recording_destination_first_frame_ts_filepath(&encoder, video_destination_id, ts_filepath.c_str());
}
}
int audio_max_frame_size = 1024;
@@ -4527,6 +4531,11 @@ int main(int argc, char **argv) {
replay_recording_start_result = start_recording_create_streams(replay_recording_filepath.c_str(), arg_parser, video_codec_context, audio_tracks, hdr, video_sources);
if(replay_recording_start_result.av_format_context) {
const size_t video_recording_destination_id = gsr_encoder_add_recording_destination(&encoder, video_codec_context, replay_recording_start_result.av_format_context, replay_recording_start_result.video_stream, video_frame->pts);
if(arg_parser.write_first_frame_ts && video_recording_destination_id != (size_t)-1) {
std::string ts_filepath = replay_recording_filepath + ".ts";
gsr_encoder_set_recording_destination_first_frame_ts_filepath(&encoder, video_recording_destination_id, ts_filepath.c_str());
}
if(video_recording_destination_id != (size_t)-1)
replay_recording_items.push_back(video_recording_destination_id);