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 <marcosashiglesias@gmail.com>
This commit is contained in:
@@ -2654,6 +2654,12 @@ writing is completed. Default is disabled.
|
|||||||
@item protocol_opts @var{options_list}
|
@item protocol_opts @var{options_list}
|
||||||
Set protocol options as a :-separated list of key=value parameters. Values
|
Set protocol options as a :-separated list of key=value parameters. Values
|
||||||
containing the @code{:} special character must be escaped.
|
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
|
@end table
|
||||||
|
|
||||||
@subsection Examples
|
@subsection Examples
|
||||||
|
|||||||
@@ -21,16 +21,24 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
#include <time.h>
|
#include <time.h>
|
||||||
|
#ifdef _WIN32
|
||||||
|
#include <sys/utime.h>
|
||||||
|
#else
|
||||||
|
#include <sys/time.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
#include "config_components.h"
|
#include "config_components.h"
|
||||||
|
|
||||||
|
#include "libavutil/avutil.h"
|
||||||
#include "libavutil/intreadwrite.h"
|
#include "libavutil/intreadwrite.h"
|
||||||
#include "libavutil/avstring.h"
|
#include "libavutil/avstring.h"
|
||||||
#include "libavutil/bprint.h"
|
#include "libavutil/bprint.h"
|
||||||
#include "libavutil/dict.h"
|
#include "libavutil/dict.h"
|
||||||
#include "libavutil/log.h"
|
#include "libavutil/log.h"
|
||||||
|
#include "libavutil/mathematics.h"
|
||||||
#include "libavutil/mem.h"
|
#include "libavutil/mem.h"
|
||||||
#include "libavutil/opt.h"
|
#include "libavutil/opt.h"
|
||||||
|
#include "libavutil/parseutils.h"
|
||||||
#include "libavutil/pixdesc.h"
|
#include "libavutil/pixdesc.h"
|
||||||
#include "libavutil/time_internal.h"
|
#include "libavutil/time_internal.h"
|
||||||
#include "avformat.h"
|
#include "avformat.h"
|
||||||
@@ -50,8 +58,36 @@ typedef struct VideoMuxData {
|
|||||||
const char *muxer;
|
const char *muxer;
|
||||||
int use_rename;
|
int use_rename;
|
||||||
AVDictionary *protocol_opts;
|
AVDictionary *protocol_opts;
|
||||||
|
int update_filemtime;
|
||||||
|
int64_t creation_ts; /**< creation_time in microseconds since epoch */
|
||||||
} VideoMuxData;
|
} 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)
|
static int write_header(AVFormatContext *s)
|
||||||
{
|
{
|
||||||
VideoMuxData *img = s->priv_data;
|
VideoMuxData *img = s->priv_data;
|
||||||
@@ -75,6 +111,29 @@ static int write_header(AVFormatContext *s)
|
|||||||
}
|
}
|
||||||
img->img_number = img->start_img_number;
|
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;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -143,6 +202,7 @@ static int write_packet(AVFormatContext *s, AVPacket *pkt)
|
|||||||
AVIOContext *pb[4] = {0};
|
AVIOContext *pb[4] = {0};
|
||||||
char* target[4] = {0};
|
char* target[4] = {0};
|
||||||
char* tmp[4] = {0};
|
char* tmp[4] = {0};
|
||||||
|
char* filepaths[4] = {0};
|
||||||
AVCodecParameters *par = s->streams[pkt->stream_index]->codecpar;
|
AVCodecParameters *par = s->streams[pkt->stream_index]->codecpar;
|
||||||
const AVPixFmtDescriptor *desc = av_pix_fmt_desc_get(par->format);
|
const AVPixFmtDescriptor *desc = av_pix_fmt_desc_get(par->format);
|
||||||
int ret, i;
|
int ret, i;
|
||||||
@@ -204,6 +264,14 @@ static int write_packet(AVFormatContext *s, AVPacket *pkt)
|
|||||||
goto fail;
|
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)
|
if (!img->split_planes || i+1 >= desc->nb_components)
|
||||||
break;
|
break;
|
||||||
filename.str[filename.len - 1] = "UVAx"[i];
|
filename.str[filename.len - 1] = "UVAx"[i];
|
||||||
@@ -246,6 +314,34 @@ static int write_packet(AVFormatContext *s, AVPacket *pkt)
|
|||||||
av_freep(&target[i]);
|
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++;
|
img->img_number++;
|
||||||
return 0;
|
return 0;
|
||||||
|
|
||||||
@@ -255,6 +351,7 @@ fail:
|
|||||||
for (i = 0; i < FF_ARRAY_ELEMS(pb); i++) {
|
for (i = 0; i < FF_ARRAY_ELEMS(pb); i++) {
|
||||||
av_freep(&tmp[i]);
|
av_freep(&tmp[i]);
|
||||||
av_freep(&target[i]);
|
av_freep(&target[i]);
|
||||||
|
av_freep(&filepaths[i]);
|
||||||
if (pb[i])
|
if (pb[i])
|
||||||
ff_format_io_close(s, &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 },
|
{ "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 },
|
{ "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 },
|
{ "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 },
|
{ NULL },
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -503,6 +503,21 @@ lavf_image2pipe(){
|
|||||||
do_avconv_crc $file -auto_conversion_filters $DEC_OPTS -f image2pipe -i $target_path/$file
|
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(){
|
lavf_video(){
|
||||||
t="${test#lavf-}"
|
t="${test#lavf-}"
|
||||||
outdir="tests/data/lavf"
|
outdir="tests/data/lavf"
|
||||||
|
|||||||
@@ -620,8 +620,16 @@ FATE_IMAGE += $(FATE_IMAGE-yes)
|
|||||||
FATE_IMAGE_PROBE += $(FATE_IMAGE_PROBE-yes)
|
FATE_IMAGE_PROBE += $(FATE_IMAGE_PROBE-yes)
|
||||||
FATE_IMAGE_TRANSCODE += $(FATE_IMAGE_TRANSCODE-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_FFMPEG += $(FATE_IMAGE)
|
||||||
FATE_SAMPLES_FFPROBE += $(FATE_IMAGE_PROBE)
|
FATE_SAMPLES_FFPROBE += $(FATE_IMAGE_PROBE)
|
||||||
FATE_SAMPLES_FFMPEG_FFPROBE += $(FATE_IMAGE_TRANSCODE)
|
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)
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
1704067200
|
||||||
|
1704067201
|
||||||
|
1704067202
|
||||||
Reference in New Issue
Block a user