Skip to content

Instantly share code, notes, and snippets.

@briancline
Last active February 2, 2023 22:10
Embed
What would you like to do?
patch for ffmpeg 5.1: allow use of `%t` in image2 output filenames to include video timestamp of the frame

ffmpeg patch: video timestamp in image2 output filenames (2022-10-27)

What it do

When extracting individual frames from a video file as image outputs, allows the use of %t in the output filename to be substituted at runtime with the timecode for each frame (for example 00.01.02.033).

Therefore an output filename of wtf_%t.jpg looks like wtf_00.11.22.333.jpg.

This is in lieu of using %d, which is just an output counter unrelated to frame numbers or timestamps, so they cannot be used together.

Quite useful, but seems stalled upstream due to some indecision about whether to use %t or try to find a better more robust means of specifying these markers, and probably just not a priority for them in general.

Example Use

Write one thumbnail per 5 seconds of video (thumb_00.00.00.000.jpg, thumb_00.00.05.000.jpg, etc.):

ffmpeg \
  -an \
  -copyts \
  -i source.mp4 \
  -vf fps=1/5 \
  -f image2 \
  'thumb_%t.jpg'

Credits

This patch has a very long and storied history and was written/readopted/tweaked by various ffmpeg contributors over the years. I've simply re-applied it atop the much more recent v5.1.2 with minor updates where needed and offered a copy of the updated patch here.

The other people who put the real work into this patch before I got my hands on it:

diff -Nurp orig-ffmpeg-5.1.2/doc/muxers.texi ffmpeg-5.1.2/doc/muxers.texi
--- orig-ffmpeg-5.1.2/doc/muxers.texi 2022-10-26 20:41:23.448158621 -0500
+++ ffmpeg-5.1.2/doc/muxers.texi 2022-10-26 20:43:33.992821906 -0500
@@ -1392,6 +1392,9 @@ If the pattern contains "%d" or "%0@var{
the file list specified will contain the number 1, all the following
numbers will be sequential.
+The pattern can also contain "%t" which writes out the timestamp of the
+frame in the format "HH.mm.ss.fff"
+
The pattern may contain a suffix which is used to automatically
determine the format of the image files to write.
diff -Nurp orig-ffmpeg-5.1.2/libavformat/avformat.h ffmpeg-5.1.2/libavformat/avformat.h
--- orig-ffmpeg-5.1.2/libavformat/avformat.h 2022-10-26 20:41:22.344119243 -0500
+++ ffmpeg-5.1.2/libavformat/avformat.h 2022-10-26 20:45:29.792968466 -0500
@@ -2733,10 +2733,11 @@ void av_dump_format(AVFormatContext *ic,
* @param path numbered sequence string
* @param number frame number
* @param flags AV_FRAME_FILENAME_FLAGS_*
+ * @param ts frame timestamp in seconds
* @return 0 if OK, -1 on format error
*/
int av_get_frame_filename2(char *buf, int buf_size,
- const char *path, int number, int flags);
+ const char *path, int number, int flags, int64_t t);
int av_get_frame_filename(char *buf, int buf_size,
const char *path, int number);
diff -Nurp orig-ffmpeg-5.1.2/libavformat/img2enc.c ffmpeg-5.1.2/libavformat/img2enc.c
--- orig-ffmpeg-5.1.2/libavformat/img2enc.c 2022-10-26 20:41:22.400121240 -0500
+++ ffmpeg-5.1.2/libavformat/img2enc.c 2022-10-26 21:04:27.033262565 -0500
@@ -140,9 +140,11 @@ static int write_packet(AVFormatContext
AVIOContext *pb[4] = {0};
char filename[1024];
AVCodecParameters *par = s->streams[pkt->stream_index]->codecpar;
+ AVStream *stream = s->streams[pkt->stream_index];
const AVPixFmtDescriptor *desc = av_pix_fmt_desc_get(par->format);
int ret, i;
int nb_renames = 0;
+ int64_t ts = av_rescale_q(pkt->pts, stream->time_base, AV_TIME_BASE_Q);
AVDictionary *options = NULL;
if (img->update) {
@@ -157,13 +159,14 @@ static int write_packet(AVFormatContext
return AVERROR(EINVAL);
}
} else if (img->frame_pts) {
- if (av_get_frame_filename2(filename, sizeof(filename), s->url, pkt->pts, AV_FRAME_FILENAME_FLAGS_MULTIPLE) < 0) {
+ if (av_get_frame_filename2(filename, sizeof(filename), s->url, pkt->pts, AV_FRAME_FILENAME_FLAGS_MULTIPLE, 0) < 0) {
av_log(s, AV_LOG_ERROR, "Cannot write filename by pts of the frames.");
return AVERROR(EINVAL);
}
} else if (av_get_frame_filename2(filename, sizeof(filename), s->url,
img->img_number,
- AV_FRAME_FILENAME_FLAGS_MULTIPLE) < 0) {
+ AV_FRAME_FILENAME_FLAGS_MULTIPLE,
+ ts) < 0) {
if (img->img_number == img->start_img_number) {
av_log(s, AV_LOG_WARNING, "The specified filename '%s' does not contain an image sequence pattern or a pattern is invalid.\n", s->url);
av_log(s, AV_LOG_WARNING,
diff -Nurp orig-ffmpeg-5.1.2/libavformat/utils.c ffmpeg-5.1.2/libavformat/utils.c
--- orig-ffmpeg-5.1.2/libavformat/utils.c 2022-10-26 20:41:22.348119385 -0500
+++ ffmpeg-5.1.2/libavformat/utils.c 2022-10-26 21:02:46.913755214 -0500
@@ -291,15 +291,17 @@ uint64_t ff_parse_ntp_time(uint64_t ntp_
return (sec * 1000000) + usec;
}
-int av_get_frame_filename2(char *buf, int buf_size, const char *path, int number, int flags)
+int av_get_frame_filename2(char *buf, int buf_size, const char *path, int number, int flags, int64_t ts)
{
const char *p;
char *q, buf1[20], c;
- int nd, len, percentd_found;
+ int nd, len, percentd_found, percentt_found;
+ int hours, mins, secs, ms;
q = buf;
p = path;
percentd_found = 0;
+ percentt_found = 0;
for (;;) {
c = *p++;
if (c == '\0')
@@ -331,6 +333,26 @@ int av_get_frame_filename2(char *buf, in
memcpy(q, buf1, len);
q += len;
break;
+ case 't':
+ if (percentd_found)
+ goto fail;
+ if (ts < 0)
+ goto fail;
+ percentt_found = 1;
+ ms = (ts/1000)%1000;
+ ts /= AV_TIME_BASE;
+ secs = ts % 60;
+ ts /= 60;
+ mins = ts % 60;
+ ts /= 60;
+ hours = ts;
+ snprintf(buf1, sizeof(buf1), "%02d.%02d.%02d.%03d", hours, mins, secs, ms);
+ len = strlen(buf1);
+ if ((q - buf + len) > buf_size - 1)
+ goto fail;
+ memcpy(q, buf1, len);
+ q += len;
+ break;
default:
goto fail;
}
@@ -340,7 +362,7 @@ addchar:
*q++ = c;
}
}
- if (!percentd_found)
+ if (!(percentd_found || percentt_found))
goto fail;
*q = '\0';
return 0;
@@ -351,7 +373,7 @@ fail:
int av_get_frame_filename(char *buf, int buf_size, const char *path, int number)
{
- return av_get_frame_filename2(buf, buf_size, path, number, 0);
+ return av_get_frame_filename2(buf, buf_size, path, number, 0, 0);
}
void av_url_split(char *proto, int proto_size,
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment