This is an automated email from the git hooks/post-receive script.
Git pushed a commit to branch master
in repository ffmpeg.
The following commit(s) were added to refs/heads/master by this push:
new 2f779272e0 libavformat/img2enc: add update_filemtime option
2f779272e0 is described below
commit 2f779272e061fbdf2725009f9e327c2f7a6b0a61
Author: marcos ashton <[email protected]>
AuthorDate: Mon Mar 23 16:03:40 2026 +0000
Commit: michaelni <[email protected]>
CommitDate: Fri Jul 3 19:47:10 2026 +0000
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]>
---
doc/muxers.texi | 6 +++
libavformat/img2enc.c | 98 ++++++++++++++++++++++++++++++++++++
tests/fate-run.sh | 15 ++++++
tests/fate/image.mak | 10 +++-
tests/ref/fate/img2-update-filemtime | 3 ++
5 files changed, 131 insertions(+), 1 deletion(-)
diff --git a/doc/muxers.texi b/doc/muxers.texi
index 92d707ad9f..5dc46ff291 100644
--- a/doc/muxers.texi
+++ b/doc/muxers.texi
@@ -2654,6 +2654,12 @@ writing is completed. Default is disabled.
@item protocol_opts @var{options_list}
Set protocol options as a :-separated list of key=value parameters. Values
containing the @code{:} special character must be escaped.
+
+@item update_filemtime @var{bool}
+If set to 1, set each output file's modification time to the
+@code{creation_time} metadata value plus the frame's PTS offset.
+If @code{creation_time} is missing or unparsable, a warning is
+logged and the option is ignored. Default value is 0.
@end table
@subsection Examples
diff --git a/libavformat/img2enc.c b/libavformat/img2enc.c
index b11f62d85d..1296da8ec7 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,36 @@ 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)
+{
+ int64_t sec = ts_us / 1000000;
+ int64_t usec = ts_us % 1000000;
+
+ if (usec < 0) {
+ sec--;
+ usec += 1000000;
+ }
+
+#ifdef _WIN32
+ struct _utimbuf ut;
+ ut.actime = sec;
+ ut.modtime = sec;
+ if (_utime(path, &ut) < 0)
+#else
+ struct timeval times[2] = {
+ { .tv_sec = sec, .tv_usec = usec },
+ { .tv_sec = sec, .tv_usec = usec },
+ };
+ 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 +111,29 @@ static int write_header(AVFormatContext *s)
}
img->img_number = img->start_img_number;
+ if (img->update_filemtime) {
+ const char *proto = avio_find_protocol_name(s->url);
+ AVDictionaryEntry *entry;
+ int64_t parsed_ts;
+
+ if (!proto || strcmp(proto, "file")) {
+ av_log(s, AV_LOG_WARNING,
+ "update_filemtime is only supported for local files, "
+ "it will be ignored\n");
+ img->update_filemtime = 0;
+ } else {
+ 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 +202,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 +264,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 +314,34 @@ 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;
+ int skip = 0;
+
+ if (pkt->pts != AV_NOPTS_VALUE) {
+ int64_t offset = av_rescale_q(pkt->pts, st->time_base,
+ AV_TIME_BASE_Q);
+ if (offset == INT64_MIN ||
+ (offset > 0 && img->creation_ts > INT64_MAX - offset) ||
+ (offset < 0 && img->creation_ts < INT64_MIN - offset)) {
+ av_log(s, AV_LOG_WARNING,
+ "Integer overflow computing file mtime, skipping\n");
+ skip = 1;
+ } else {
+ frame_ts += offset;
+ }
+ }
+
+ if (!skip) {
+ 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 +351,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 +378,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 a0aee7158d..d0726c64dd 100755
--- a/tests/fate-run.sh
+++ b/tests/fate-run.sh
@@ -503,6 +503,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,format=gray8" \
+ -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 be803094da..e9fe059ead 100644
--- a/tests/fate/image.mak
+++ b/tests/fate/image.mak
@@ -620,8 +620,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 FORMAT_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_FFMPEG_FFPROBE += $(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
_______________________________________________
ffmpeg-cvslog mailing list -- [email protected]
To unsubscribe send an email to [email protected]