PR #22592 opened by MarcosAsh URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/22592 Patch URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/22592.patch
Closes #22537. Adds a new `-update_filemtime` boolean option to the image2 muxer. When enabled, each output file's modification time is set to the value of the creation_time metadata plus the frame's PTS offset. The use case is extracting frames from dashcam or action camera footage where the wall-clock timestamp matters -- photo managers and file browsers use mtime for sorting, and right now extracted frames all get the current time instead of the capture time. Usage example: ``` ffmpeg -i dashcam.mp4 -metadata creation_time="2024-06-15T14:30:00Z" \ -update_filemtime 1 frame_%04d.jpg ``` If creation_time metadata is missing or unparseable, a warning is logged and the option is silently disabled. When PTS is unavailable for a frame, the base creation time is used without offset. The implementation uses utimes() on POSIX (microsecond precision) and _utime() on Windows (second precision). File paths are saved during the open loop and timestamps are applied after writes complete (and after atomic rename, when -atomic_writing is also used). Split-planes mode is also handled. Includes a FATE roundtrip test: writes frames with a known creation_time, reads them back with the demuxer's -ts_from_file option, and verifies the PTS values match expected timestamps. >From 9d0087fa72b22d8e8b859d2cefe1cff411765757 Mon Sep 17 00:00:00 2001 From: marcos ashton <[email protected]> Date: Mon, 23 Mar 2026 16:03:40 +0000 Subject: [PATCH] libavformat/img2enc: add update_filemtime option Add a new boolean option -update_filemtime to the image2 muxer that sets each output file's modification time based on the creation_time metadata plus the frame's PTS offset. This is useful when extracting frames from dashcam or action camera footage where wall-clock timestamps should be preserved on the output files, allowing photo management tools to sort frames by capture time without post-processing. The option requires creation_time metadata to be set (via -metadata creation_time=...). If not present, a warning is logged and the option is silently disabled. When PTS is unavailable, the creation time is used as-is without frame offset. Uses utimes() on POSIX and _utime() on Windows to set file timestamps with microsecond and second precision respectively. Includes a FATE roundtrip test that writes frames with a known creation_time, reads them back using the demuxer's -ts_from_file option, and verifies the PTS values match the expected timestamps. Closes: https://code.ffmpeg.org/FFmpeg/FFmpeg/issues/22537 Signed-off-by: marcos ashton <[email protected]> --- libavformat/img2enc.c | 69 ++++++++++++++++++++++++++++ tests/fate-run.sh | 15 ++++++ tests/fate/image.mak | 10 +++- tests/ref/fate/img2-update-filemtime | 3 ++ 4 files changed, 96 insertions(+), 1 deletion(-) create mode 100644 tests/ref/fate/img2-update-filemtime diff --git a/libavformat/img2enc.c b/libavformat/img2enc.c index b11f62d85d..4ae8bcc976 100644 --- a/libavformat/img2enc.c +++ b/libavformat/img2enc.c @@ -21,16 +21,24 @@ */ #include <time.h> +#ifdef _WIN32 +#include <sys/utime.h> +#else +#include <sys/time.h> +#endif #include "config_components.h" +#include "libavutil/avutil.h" #include "libavutil/intreadwrite.h" #include "libavutil/avstring.h" #include "libavutil/bprint.h" #include "libavutil/dict.h" #include "libavutil/log.h" +#include "libavutil/mathematics.h" #include "libavutil/mem.h" #include "libavutil/opt.h" +#include "libavutil/parseutils.h" #include "libavutil/pixdesc.h" #include "libavutil/time_internal.h" #include "avformat.h" @@ -50,8 +58,28 @@ typedef struct VideoMuxData { const char *muxer; int use_rename; AVDictionary *protocol_opts; + int update_filemtime; + int64_t creation_ts; /**< creation_time in microseconds since epoch */ } VideoMuxData; +static void set_file_mtime(AVFormatContext *s, const char *path, int64_t ts_us) +{ +#ifdef _WIN32 + struct _utimbuf ut; + ut.actime = ts_us / 1000000; + ut.modtime = ts_us / 1000000; + if (_utime(path, &ut) < 0) +#else + struct timeval times[2] = { + { .tv_sec = ts_us / 1000000, .tv_usec = ts_us % 1000000 }, + { .tv_sec = ts_us / 1000000, .tv_usec = ts_us % 1000000 }, + }; + if (utimes(path, times) < 0) +#endif + av_log(s, AV_LOG_WARNING, + "Failed to set file modification time for %s\n", path); +} + static int write_header(AVFormatContext *s) { VideoMuxData *img = s->priv_data; @@ -75,6 +103,21 @@ static int write_header(AVFormatContext *s) } img->img_number = img->start_img_number; + if (img->update_filemtime) { + AVDictionaryEntry *entry; + int64_t parsed_ts; + + entry = av_dict_get(s->metadata, "creation_time", NULL, 0); + if (!entry || av_parse_time(&parsed_ts, entry->value, 0) < 0) { + av_log(s, AV_LOG_WARNING, + "No valid creation_time metadata found, " + "update_filemtime will be ignored\n"); + img->update_filemtime = 0; + } else { + img->creation_ts = parsed_ts; + } + } + return 0; } @@ -143,6 +186,7 @@ static int write_packet(AVFormatContext *s, AVPacket *pkt) AVIOContext *pb[4] = {0}; char* target[4] = {0}; char* tmp[4] = {0}; + char* filepaths[4] = {0}; AVCodecParameters *par = s->streams[pkt->stream_index]->codecpar; const AVPixFmtDescriptor *desc = av_pix_fmt_desc_get(par->format); int ret, i; @@ -204,6 +248,14 @@ static int write_packet(AVFormatContext *s, AVPacket *pkt) goto fail; } + if (img->update_filemtime) { + filepaths[i] = av_strdup(filename.str); + if (!filepaths[i]) { + ret = AVERROR(ENOMEM); + goto fail; + } + } + if (!img->split_planes || i+1 >= desc->nb_components) break; filename.str[filename.len - 1] = "UVAx"[i]; @@ -246,6 +298,21 @@ static int write_packet(AVFormatContext *s, AVPacket *pkt) av_freep(&target[i]); } + if (img->update_filemtime) { + AVStream *st = s->streams[pkt->stream_index]; + int64_t frame_ts = img->creation_ts; + + if (pkt->pts != AV_NOPTS_VALUE) + frame_ts += av_rescale_q(pkt->pts, st->time_base, + AV_TIME_BASE_Q); + + for (i = 0; i < 4 && filepaths[i]; i++) + set_file_mtime(s, filepaths[i], frame_ts); + } + + for (i = 0; i < FF_ARRAY_ELEMS(filepaths); i++) + av_freep(&filepaths[i]); + img->img_number++; return 0; @@ -255,6 +322,7 @@ fail: for (i = 0; i < FF_ARRAY_ELEMS(pb); i++) { av_freep(&tmp[i]); av_freep(&target[i]); + av_freep(&filepaths[i]); if (pb[i]) ff_format_io_close(s, &pb[i]); } @@ -281,6 +349,7 @@ static const AVOption muxoptions[] = { { "frame_pts", "use current frame pts for filename", OFFSET(frame_pts), AV_OPT_TYPE_BOOL, { .i64 = 0 }, 0, 1, ENC }, { "atomic_writing", "write files atomically (using temporary files and renames)", OFFSET(use_rename), AV_OPT_TYPE_BOOL, { .i64 = 0 }, 0, 1, ENC }, { "protocol_opts", "specify protocol options for the opened files", OFFSET(protocol_opts), AV_OPT_TYPE_DICT, {0}, 0, 0, ENC }, + { "update_filemtime", "set output file mtime from creation_time metadata plus frame offset", OFFSET(update_filemtime), AV_OPT_TYPE_BOOL, { .i64 = 0 }, 0, 1, ENC }, { NULL }, }; diff --git a/tests/fate-run.sh b/tests/fate-run.sh index cd66cd059c..816772c700 100755 --- a/tests/fate-run.sh +++ b/tests/fate-run.sh @@ -469,6 +469,21 @@ lavf_image2pipe(){ do_avconv_crc $file -auto_conversion_filters $DEC_OPTS -f image2pipe -i $target_path/$file } +img2_update_filemtime(){ + outdir="tests/data/lavf" + file=${outdir}/img2_mtime_%03d.pgm + cleanfiles="$cleanfiles ${outdir}/img2_mtime_001.pgm ${outdir}/img2_mtime_002.pgm ${outdir}/img2_mtime_003.pgm" + ffmpeg -f lavfi -i color=c=black:s=2x2:r=1:d=3 \ + -c:v pgm \ + -metadata creation_time="2024-01-01T00:00:00.000000Z" \ + -update_filemtime 1 \ + -y $file || return + probe -f image2 -ts_from_file sec \ + -show_entries packet=pts \ + -of csv=p=0 \ + -i $file +} + lavf_video(){ t="${test#lavf-}" outdir="tests/data/lavf" diff --git a/tests/fate/image.mak b/tests/fate/image.mak index 334c421140..962af8c72b 100644 --- a/tests/fate/image.mak +++ b/tests/fate/image.mak @@ -607,8 +607,16 @@ FATE_IMAGE += $(FATE_IMAGE-yes) FATE_IMAGE_PROBE += $(FATE_IMAGE_PROBE-yes) FATE_IMAGE_TRANSCODE += $(FATE_IMAGE_TRANSCODE-yes) +FATE_IMG2_MUXER-$(call ALLYES, LAVFI_INDEV COLOR_FILTER PGM_ENCODER \ + IMAGE2_MUXER IMAGE2_DEMUXER FILE_PROTOCOL FFPROBE) \ + += fate-img2-update-filemtime +fate-img2-update-filemtime: CMD = img2_update_filemtime +fate-img2-update-filemtime: REF = $(SRC_PATH)/tests/ref/fate/img2-update-filemtime + +FATE_AVCONV += $(FATE_IMG2_MUXER-yes) + FATE_SAMPLES_FFMPEG += $(FATE_IMAGE) FATE_SAMPLES_FFPROBE += $(FATE_IMAGE_PROBE) FATE_SAMPLES_FFMPEG_FFPROBE += $(FATE_IMAGE_TRANSCODE) -fate-image: $(FATE_IMAGE) $(FATE_IMAGE_PROBE) $(FATE_IMAGE_TRANSCODE) +fate-image: $(FATE_IMAGE) $(FATE_IMAGE_PROBE) $(FATE_IMAGE_TRANSCODE) $(FATE_IMG2_MUXER-yes) diff --git a/tests/ref/fate/img2-update-filemtime b/tests/ref/fate/img2-update-filemtime new file mode 100644 index 0000000000..630eccf559 --- /dev/null +++ b/tests/ref/fate/img2-update-filemtime @@ -0,0 +1,3 @@ +1704067200 +1704067201 +1704067202 -- 2.52.0 _______________________________________________ ffmpeg-devel mailing list -- [email protected] To unsubscribe send an email to [email protected]
