Using FFMPEG to Concatenate and Embed Subtitles

I recently upgraded my drone to a DJI Mavic Air 2. Among other things, it can create h.265 videos directly. It still uses the MP4 container format and the separate SRT format for storing video subtitles, the flight data. Following most camera standards, it creates video files that are individually smaller than 4 GB, which works out to be about five minutes in 4k video.

If I want to upload a longer raw video to social media, the video files need to be concatenated before uploading.

Concatenating the video with FFMPEG has been something I’ve known how to do for a long time using either of two methods. Today I learned how to properly embed the subtitles in either the MKV or MP4 container format.

The MP4 format is more widely supported than the MKV format, but is less flexible as to what it can contain.  The MKV (Matroska Multimedia Container) container format can hold almost any type of media, and so I’m able to copy the SRT format directly. The MP4 (MPEG-4 Part 14) container format only supports a limited selection of subtitle formats, so I’m required to have FFMPEG convert the SRT stream to a MP4 compatible stream.  If you are interested in video container formats, these tables are very helpful.

I’ll give several examples using the two video files and their associated subtitle files created by the drone named DJI00001.MP4, DJI00002.MP4, DJI00001.SRT, and DJI00002.SRT. The method I’m using should work for any number of files, up to the largest filesize you can store on your filesystem.

To simply concatenate the video files, create a text input file (I’m using mp4files.txt) with the contents as follows

file DJI00001.MP4
file DJI00001.MP4

then use the ffmpeg command to create a new concatenated file.

ffmpeg -f concat -safe 0 -i mp4files.txt -c copy ConcatenatedVideo.MP4

If you want to embed the subtitles, you need to create a second text file, do some stream mapping, and specify what format the subtitles should be. In this case I’m using srtfiles.txt

file DJI00001.SRT
file DJI00002.SRT

My FFMPEG command to create an MP4 file gets a lot more involved because now I’m specifying multiple inputs and have to specify the subtitle format.

ffmpeg -f concat -safe 0 -i mp4files.txt -f concat -safe 0 -i srtfiles.txt -map 0:v -map 1 -c:v copy -c:s mov_text ConcatenatedVideo.MP4

The FFMPEG command to create an MKV command is only a tiny bit different, and the resulting file is only a tiny bit smaller.

ffmpeg -f concat -safe 0 -i mp4files.txt -f concat -safe 0 -i srtfiles.txt -map 0:v -map 1 -c:v copy -c:s copy ConcatenatedVideo.MKV

When playing the ConcatenatedVideo files on my local machine, I can now enable or disable the closed caption track properly in the player for either format. Unfortunately in my initial testing with YouTube, neither format maintains the second stream of subtitles.

This is not all a waste of time and effort, because an advantage of embedding the subtitles into the container format is that the timing has been matched to the video, and can now be extracted in a concatenated form for use with YouTube.

ffmpeg -i ConcatenatedVideo.MKV -c copy ConcatenatedVideo.SRT

You can exclude the “-c copy” when extracting the subtitles and FFMPEG fill run it through its subrip codec and produce nearly identical results. It will only work with the MKV file because the subtitle format stored in the MP4 file is not easily converted to a SRT file.

Using the -f concat option invokes the concat demuxer in FFMPEG, which has the limitation that the format needs to be exactly the same for each file. If there are any changes between files you want to concatenate, you must use a more involved command invoking the concat filter. In a different project I ran into a command issue with the concat filter command when the command got to be much over 900 characters long.


Raspberry Pi ZeroW Camera Focus with FFMPEG

I wanted a quick and dirty method to test my camera module installation on my Raspberry Pi ZeroW installation. I don’t have a monitor connected to the Raspberry, and explicitly did not install the desktop version of the operating system. This is especially important because the camera itself may not be properly focused after installation in the case, and the only way to easily focus the camera is with a video stream allowing you to make small adjustments and see them nearly real time.

I’ve used FFMPEG for years as it handles almost any kind of video or audio I can throw at it. I use VLC on my desktop machine for similar reasons.

I did a quick install of ffmpeg on my Pi with the following command, allowing it to install all the requirements, adding up to almost 126 new packages and 56MB that needed to be downloaded and installed.

sudo apt-get install ffmpeg -y

After it finished installing, I was able to run the following command with the address being my desktop computer.

ffmpeg -f video4linux2 -input_format h264 -video_size 1280x720 -framerate 30 -i /dev/video0 -vcodec copy -an -f mpegts udp://

On my desktop computer I ran VLC, under the Media menu, selected Open Network Stream, and opened:


2019-09-23 (1)2019-09-23 (2)

What I’m doing is to use FFMPEG to pull video from the device and push it using UDP datagrams at my desktop on port 5000. Then VLC opens a port on the local machine at port 5000 to receive the datagrams and it decodes and displays the video. An interesting thing about this method is that I can stop transmitting from the raspberry, then restart it, and VLC will accept the packets since UDP is a connectionless protocol.

What really surprised me was that when I logged in a second time to my Raspberry Pi to view the CPU usage for streaming, it was only running around 12% of the CPU. I was interested in knowing what native formats the camera supported..

ffmpeg -f v4l2 -list_formats all -i /dev/video0
ffmpeg version 4.1.4-1+rpt1~deb10u1 Copyright (c) 2000-2019 the FFmpeg developers
  built with gcc 8 (Raspbian 8.3.0-6+rpi1)
  configuration: --prefix=/usr --extra-version='1+rpt1~deb10u1' --toolchain=hardened --libdir=/usr/lib/arm-linux-gnueabihf --incdir=/usr/include/arm-linux-gnueabihf --arch=arm --enable-gpl --disable-stripping --enable-avresample --disable-filter=resample --enable-avisynth --enable-gnutls --enable-ladspa --enable-libaom --enable-libass --enable-libbluray --enable-libbs2b --enable-libcaca --enable-libcdio --enable-libcodec2 --enable-libflite --enable-libfontconfig --enable-libfreetype --enable-libfribidi --enable-libgme --enable-libgsm --enable-libjack --enable-libmp3lame --enable-libmysofa --enable-libopenjpeg --enable-libopenmpt --enable-libopus --enable-libpulse --enable-librsvg --enable-librubberband --enable-libshine --enable-libsnappy --enable-libsoxr --enable-libspeex --enable-libssh --enable-libtheora --enable-libtwolame --enable-libvidstab --enable-libvorbis --enable-libvpx --enable-libwavpack --enable-libwebp --enable-libx265 --enable-libxml2 --enable-libxvid --enable-libzmq --enable-libzvbi --enable-lv2 --enable-omx --enable-openal --enable-opengl --enable-sdl2 --enable-omx-rpi --enable-mmal --enable-libdc1394 --enable-libdrm --enable-libiec61883 --enable-chromaprint --enable-frei0r --enable-libx264 --enable-shared
  libavutil      56. 22.100 / 56. 22.100
  libavcodec     58. 35.100 / 58. 35.100
  libavformat    58. 20.100 / 58. 20.100
  libavdevice    58.  5.100 / 58.  5.100
  libavfilter     7. 40.101 /  7. 40.101
  libavresample   4.  0.  0 /  4.  0.  0
  libswscale      5.  3.100 /  5.  3.100
  libswresample   3.  3.100 /  3.  3.100
  libpostproc    55.  3.100 / 55.  3.100
[video4linux2,v4l2 @ 0x2367e40] Raw       :     yuv420p :     Planar YUV 4:2:0 : {32-3280, 2}x{32-2464, 2}
[video4linux2,v4l2 @ 0x2367e40] Raw       :     yuyv422 :           YUYV 4:2:2 : {32-3280, 2}x{32-2464, 2}
[video4linux2,v4l2 @ 0x2367e40] Raw       :       rgb24 :     24-bit RGB 8-8-8 : {32-3280, 2}x{32-2464, 2}
[video4linux2,v4l2 @ 0x2367e40] Compressed:       mjpeg :            JFIF JPEG : {32-3280, 2}x{32-2464, 2}
[video4linux2,v4l2 @ 0x2367e40] Compressed:        h264 :                H.264 : {32-3280, 2}x{32-2464, 2}
[video4linux2,v4l2 @ 0x2367e40] Compressed:       mjpeg :          Motion-JPEG : {32-3280, 2}x{32-2464, 2}
[video4linux2,v4l2 @ 0x2367e40] Raw       : Unsupported :           YVYU 4:2:2 : {32-3280, 2}x{32-2464, 2}
[video4linux2,v4l2 @ 0x2367e40] Raw       : Unsupported :           VYUY 4:2:2 : {32-3280, 2}x{32-2464, 2}
[video4linux2,v4l2 @ 0x2367e40] Raw       :     uyvy422 :           UYVY 4:2:2 : {32-3280, 2}x{32-2464, 2}
[video4linux2,v4l2 @ 0x2367e40] Raw       :        nv12 :         Y/CbCr 4:2:0 : {32-3280, 2}x{32-2464, 2}
[video4linux2,v4l2 @ 0x2367e40] Raw       :       bgr24 :     24-bit BGR 8-8-8 : {32-3280, 2}x{32-2464, 2}
[video4linux2,v4l2 @ 0x2367e40] Raw       :     yuv420p :     Planar YVU 4:2:0 : {32-3280, 2}x{32-2464, 2}
[video4linux2,v4l2 @ 0x2367e40] Raw       : Unsupported :         Y/CrCb 4:2:0 : {32-3280, 2}x{32-2464, 2}
[video4linux2,v4l2 @ 0x2367e40] Raw       :        bgr0 : 32-bit BGRA/X 8-8-8-8 : {32-3280, 2}x{32-2464, 2}
/dev/video0: Immediate exit requested

That output leads me to believe that the camera module could output either h264 or mjpeg without significant CPU overhead. What it doesn’t do is tell me efficient frame sizes. It seems to say that horizontal and vertical sizes can be anything between 32 to 3280 and 32 to 2464. I know that the specs on the camera say that it will run still frames at the high resolution, but video is significantly less.

Two Video4Linux commands that return interesting and similar results are:

pi@WimPiZeroCamera:~ $ v4l2-ctl --list-formats-ext
        Type: Video Capture

        [0]: 'YU12' (Planar YUV 4:2:0)
                Size: Stepwise 32x32 - 3280x2464 with step 2/2
        [1]: 'YUYV' (YUYV 4:2:2)
                Size: Stepwise 32x32 - 3280x2464 with step 2/2
        [2]: 'RGB3' (24-bit RGB 8-8-8)
                Size: Stepwise 32x32 - 3280x2464 with step 2/2
        [3]: 'JPEG' (JFIF JPEG, compressed)
                Size: Stepwise 32x32 - 3280x2464 with step 2/2
        [4]: 'H264' (H.264, compressed)
                Size: Stepwise 32x32 - 3280x2464 with step 2/2
        [5]: 'MJPG' (Motion-JPEG, compressed)
                Size: Stepwise 32x32 - 3280x2464 with step 2/2
        [6]: 'YVYU' (YVYU 4:2:2)
                Size: Stepwise 32x32 - 3280x2464 with step 2/2
        [7]: 'VYUY' (VYUY 4:2:2)
                Size: Stepwise 32x32 - 3280x2464 with step 2/2
        [8]: 'UYVY' (UYVY 4:2:2)
                Size: Stepwise 32x32 - 3280x2464 with step 2/2
        [9]: 'NV12' (Y/CbCr 4:2:0)
                Size: Stepwise 32x32 - 3280x2464 with step 2/2
        [10]: 'BGR3' (24-bit BGR 8-8-8)
                Size: Stepwise 32x32 - 3280x2464 with step 2/2
        [11]: 'YV12' (Planar YVU 4:2:0)
                Size: Stepwise 32x32 - 3280x2464 with step 2/2
        [12]: 'NV21' (Y/CrCb 4:2:0)
                Size: Stepwise 32x32 - 3280x2464 with step 2/2
        [13]: 'BGR4' (32-bit BGRA/X 8-8-8-8)
                Size: Stepwise 32x32 - 3280x2464 with step 2/2
pi@WimPiZeroCamera:~ $ v4l2-ctl -L

User Controls

                     brightness 0x00980900 (int)    : min=0 max=100 step=1 default=50 value=50 flags=slider
                       contrast 0x00980901 (int)    : min=-100 max=100 step=1 default=0 value=0 flags=slider
                     saturation 0x00980902 (int)    : min=-100 max=100 step=1 default=0 value=0 flags=slider
                    red_balance 0x0098090e (int)    : min=1 max=7999 step=1 default=1000 value=1000 flags=slider
                   blue_balance 0x0098090f (int)    : min=1 max=7999 step=1 default=1000 value=1000 flags=slider
                horizontal_flip 0x00980914 (bool)   : default=0 value=0
                  vertical_flip 0x00980915 (bool)   : default=0 value=0
           power_line_frequency 0x00980918 (menu)   : min=0 max=3 default=1 value=1
                                0: Disabled
                                1: 50 Hz
                                2: 60 Hz
                                3: Auto
                      sharpness 0x0098091b (int)    : min=-100 max=100 step=1 default=0 value=0 flags=slider
                  color_effects 0x0098091f (menu)   : min=0 max=15 default=0 value=0
                                0: None
                                1: Black & White
                                2: Sepia
                                3: Negative
                                4: Emboss
                                5: Sketch
                                6: Sky Blue
                                7: Grass Green
                                8: Skin Whiten
                                9: Vivid
                                10: Aqua
                                11: Art Freeze
                                12: Silhouette
                                13: Solarization
                                14: Antique
                                15: Set Cb/Cr
                         rotate 0x00980922 (int)    : min=0 max=360 step=90 default=0 value=0 flags=modify-layout
             color_effects_cbcr 0x0098092a (int)    : min=0 max=65535 step=1 default=32896 value=32896

Codec Controls

             video_bitrate_mode 0x009909ce (menu)   : min=0 max=1 default=0 value=0 flags=update
                                0: Variable Bitrate
                                1: Constant Bitrate
                  video_bitrate 0x009909cf (int)    : min=25000 max=25000000 step=25000 default=10000000 value=10000000
         repeat_sequence_header 0x009909e2 (bool)   : default=0 value=0
            h264_i_frame_period 0x00990a66 (int)    : min=0 max=2147483647 step=1 default=60 value=60
                     h264_level 0x00990a67 (menu)   : min=0 max=11 default=11 value=11
                                0: 1
                                1: 1b
                                2: 1.1
                                3: 1.2
                                4: 1.3
                                5: 2
                                6: 2.1
                                7: 2.2
                                8: 3
                                9: 3.1
                                10: 3.2
                                11: 4
                   h264_profile 0x00990a6b (menu)   : min=0 max=4 default=4 value=4
                                0: Baseline
                                1: Constrained Baseline
                                2: Main
                                4: High

Camera Controls

                  auto_exposure 0x009a0901 (menu)   : min=0 max=3 default=0 value=0
                                0: Auto Mode
                                1: Manual Mode
         exposure_time_absolute 0x009a0902 (int)    : min=1 max=10000 step=1 default=1000 value=1000
     exposure_dynamic_framerate 0x009a0903 (bool)   : default=0 value=0
             auto_exposure_bias 0x009a0913 (intmenu): min=0 max=24 default=12 value=12
                                0: -4000 (0xfffffffffffff060)
                                1: -3667 (0xfffffffffffff1ad)
                                2: -3333 (0xfffffffffffff2fb)
                                3: -3000 (0xfffffffffffff448)
                                4: -2667 (0xfffffffffffff595)
                                5: -2333 (0xfffffffffffff6e3)
                                6: -2000 (0xfffffffffffff830)
                                7: -1667 (0xfffffffffffff97d)
                                8: -1333 (0xfffffffffffffacb)
                                9: -1000 (0xfffffffffffffc18)
                                10: -667 (0xfffffffffffffd65)
                                11: -333 (0xfffffffffffffeb3)
                                12: 0 (0x0)
                                13: 333 (0x14d)
                                14: 667 (0x29b)
                                15: 1000 (0x3e8)
                                16: 1333 (0x535)
                                17: 1667 (0x683)
                                18: 2000 (0x7d0)
                                19: 2333 (0x91d)
                                20: 2667 (0xa6b)
                                21: 3000 (0xbb8)
                                22: 3333 (0xd05)
                                23: 3667 (0xe53)
                                24: 4000 (0xfa0)
      white_balance_auto_preset 0x009a0914 (menu)   : min=0 max=9 default=1 value=1
                                0: Manual
                                1: Auto
                                2: Incandescent
                                3: Fluorescent
                                4: Fluorescent H
                                5: Horizon
                                6: Daylight
                                7: Flash
                                8: Cloudy
                                9: Shade
            image_stabilization 0x009a0916 (bool)   : default=0 value=0
                iso_sensitivity 0x009a0917 (intmenu): min=0 max=4 default=0 value=0
                                0: 0 (0x0)
                                1: 100000 (0x186a0)
                                2: 200000 (0x30d40)
                                3: 400000 (0x61a80)
                                4: 800000 (0xc3500)
           iso_sensitivity_auto 0x009a0918 (menu)   : min=0 max=1 default=1 value=1
                                0: Manual
                                1: Auto
         exposure_metering_mode 0x009a0919 (menu)   : min=0 max=2 default=0 value=0
                                0: Average
                                1: Center Weighted
                                2: Spot
                     scene_mode 0x009a091a (menu)   : min=0 max=13 default=0 value=0
                                0: None
                                8: Night
                                11: Sports

JPEG Compression Controls

            compression_quality 0x009d0903 (int)    : min=1 max=100 step=1 default=30 value=30



USPS Informed Delivery Daily Digest and Netflix DVDs

I use the USPS service Informed Delivery and highly recommend it. I get a daily email from USPS with a scanned picture of most of the mail that will arrive in my mailbox that day. Occasionally the email will say that there were items that could not be scanned, but it’s very useful since I don’t check my mailbox on a daily basis, but don’t want to have important items sit for extended times.

My mailbox is fairly secure, but I’ve also read that Informed Delivery has both good and bad features for people related to mail theft or identity theft.

I’ve been getting Netflix DVDs in the mail since 2000.  I’ve always been slightly fascinated with the efficiencies the post office and Netflix have worked out. If I take a DVD mailer to my local post office here in Seattle, Netflix recognizes it has been returned the next business day. If I drop a DVD at the local post office on Tuesday, Netflix acknowledges it on Wednesday, and I’ve usually got the next DVD delivered Thursday.

Until recently the Netflix DVDs were scanned like all other mail.

2019-09-13 (3)

It appears to have changed at the beginning of September. Now there’s a pair of fixed images arriving with a link that will take you directly to Netflix.

2019-09-13 (2)

The new behavior isn’t bad since all the Netflix disk scans look very similar, but are interesting to note. I wouldn’t be surprised that the new full color image reduces bandwidth over individual scans along with added benefit of the link to Netflix.

Part 3 of ROAV Dash Cam C1 Pro

After getting the video files from my dashcam strung together at high speed I realized I was just as interested in the GPS data that was stored in .info files alongside each .mp4 file.

Some digging around in forums led me to believe that the files were fairly simple comma separated files with the headers working out to be Datetime, Latitude, Longitude, FixType, SatCount, Altitude, SpeedKph, Heading, AccelerometerX, AccelerometerY, AccelerometerZ. The Datetime field is it’s own special format, but easy enough to interpret.

Because I’ve written a GPS data app in the past, I’d learned how to write Keyhole Markup Language (KML) files, as well as learning to use ZIP routines to package them into KMZ files. My original code had hard coded the KML tags because I didn’t want to rely on an external library requirement on a limited platform. In re-using the code I updated to use the XMLLite API. Microsoft may be discontinuing support for this API as well, but at least now it’s still included with current operating systems. The advantage of using the XML API to create the XML is knowing that all of the tags are properly and consistently formed and closed. A secondary feature was that it made it much simpler to explore different data formatting options for the folders in the KML structure.

ROAV-ConCatStructureI worked on the basic idea that the most interesting data from the source files was based on speed and altitude. Then I broke the data into segments by the day, so if I store multiple days worth of files the kml will automatically have reasonable breaks in it. I create a Placemark that includes a LineString with all the GPS coordinates for the day. I calculate the distance by multiplying the reported speed by the time between points.

The calculated distance isn’t as accurate as I’d like, but the code I had from years ago didn’t seem to get more accurate distances. My web references from years ago no longer work, which is the frustrating thing about pointing to documentation on the web. The code I’d used years before was used in a bicycle gps program, which meant that the speeds were slower and the distance traveled between points may have been smaller. The data the ROAV is writing to the log file may not have as many significant digits as the raw data available from the the GPS chip itself.

I create three more Placemarks for each day, each with a simple Point defined. Max Speed, Max Altitude, and Min Altitude. Each of those data points is selected from a simple scan of the input data.


By creating a large KML file and then converting it to a KMZ, it becomes a manageable size. KMZ files load into google earth significantly faster then KML files. One of the interesting things that you can do in Google Earth is display the elevation profile of a path.


On this day you can see that I started at just under 4500 feet, drove over 6000 feet, then down to 1733 feet before ending the day near 2500 feet. When playing with the desktop app, you can drag a marker along the path and it will coordinate a marker along the elevation profile.

I’ve added this code to my ROAV-Concat program, as well as parameters that can tell the program to not output KML or MP4. this allows me to run the program and just generate the KMZ or MP4 file, though beyond during debugging I can’t think of a reason I’d not want both files generated.

I’m hoping that understanding all of this data will allow me to generate text to overlay the video with GPS data beyond what was embedded by the original dashcam.

Part 2 of FFMPEG and ROAV Dash Cam C1 Pro

While writing my software to concatenate and speed up the video files from my ROAV Dashcam I ran into an interesting issue with FFMPEG.

The -filter_complex option seems to stop parsing it’s parameters somewhere above 960 characters on the command line. I didn’t narrow down the exact point, or go digging in the FFMPEG source code to find the size. I expect this is an arbitrary buffer size in FFMPEG. I may contribute to the source code since it fails with no explanation, even when generating a report file. Learning the FFMPEG source structure in itself is a large task, meaning that I’ve not found time to do anything beyond find a workaround.

My workaround was to recognize when the command size will get long and fall back to using the -f concat option with a temporary file listing the input files instead of using the complex filtergraph.

The advantage of the complex filtergraph is twofold. It does not require a secondary input file or any cleanup. It can deal with input files that change resolution if necessary.

Here’s an example of the complex filtergraph:

ffmpeg.exe -report -i 2018_0705_101335_006.MP4 -i 2018_0705_101635_007.MP4 -i 2018_0705_101935_008.MP4 -i 2018_0705_102235_009.MP4 -i 2018_0705_102535_010.MP4 -i 2018_0705_102835_011.MP4 -i 2018_0705_103135_012.MP4 -i 2018_0705_103435_013.MP4 -i 2018_0705_103613_014A.MP4 -i 2018_0705_103615_015A.MP4 -i 2018_0705_104449_016A.MP4 -i 2018_0705_104750_017A.MP4 -i 2018_0705_105050_018A.MP4 -i 2018_0705_105350_019A.MP4 -i 2018_0705_105650_020A.MP4 -i 2018_0705_105950_021A.MP4 -i 2018_0705_110250_022A.MP4 -i 2018_0705_110550_023A.MP4 -i 2018_0705_110850_024A.MP4 -i 2018_0705_143415_025.MP4 -i 2018_0705_143716_026.MP4 -i 2018_0705_144016_027.MP4 -i 2018_0705_144316_028.MP4 -i 2018_0705_144616_029.MP4 -i 2018_0705_144916_030.MP4 -i 2018_0705_145216_031.MP4 -i 2018_0705_145516_032.MP4 -i 2018_0705_145816_033.MP4 -i 2018_0705_150116_034.MP4 -i 2018_0705_150416_035.MP4 -i 2018_0705_150716_036.MP4 -i 2018_0705_151016_037.MP4 -i 2018_0705_151316_038.MP4 -i 2018_0705_151616_039.MP4 -i 2018_0705_151916_040.MP4 -i 2018_0705_152216_041.MP4 -i 2018_0705_152516_042.MP4 -i 2018_0705_152816_043.MP4 -i 2018_0705_153116_044.MP4 -i 2018_0705_153416_045.MP4 -i 2018_0705_153716_046.MP4 -i 2018_0705_154016_047.MP4 -i 2018_0705_154316_048.MP4 -i 2018_0705_154616_049.MP4 -i 2018_0705_154916_050.MP4 -i 2018_0705_155216_051.MP4 -i 2018_0705_155516_052.MP4 -i 2018_0705_155816_053.MP4 -i 2018_0705_160116_054.MP4 -i 2018_0705_160416_055.MP4 -i 2018_0705_160716_056.MP4 -i 2018_0705_161016_057.MP4 -i 2018_0705_161316_058.MP4 -i 2018_0705_161616_059.MP4 -i 2018_0705_161916_060.MP4 -i 2018_0705_162216_061.MP4 -i 2018_0705_162516_062.MP4 -i 2018_0705_162816_063.MP4 -i 2018_0705_163116_064.MP4 -i 2018_0705_163416_065.MP4 -i 2018_0705_163716_066.MP4 -i 2018_0705_164016_067.MP4 -i 2018_0705_164316_068.MP4 -i 2018_0705_164616_069.MP4 -i 2018_0705_164916_070.MP4 -i 2018_0705_165215_071.MP4 -i 2018_0705_165516_072.MP4 -i 2018_0705_165815_073.MP4 -i 2018_0705_170115_074.MP4 -i 2018_0705_170415_075.MP4 -i 2018_0705_170715_076.MP4 -i 2018_0705_171015_077.MP4 -i 2018_0705_171315_078.MP4 -i 2018_0705_171615_079.MP4 -i 2018_0705_171915_080.MP4 -i 2018_0705_172216_081.MP4 -i 2018_0705_172515_082.MP4 -i 2018_0705_172815_083.MP4 -i 2018_0705_173115_084.MP4 -i 2018_0705_173415_085.MP4 -i 2018_0705_173715_086.MP4 -i 2018_0705_174015_087.MP4 -i 2018_0705_174315_088.MP4 -i 2018_0705_174615_089.MP4 -i 2018_0705_174915_090.MP4 -i 2018_0705_175215_091.MP4 -i 2018_0705_175515_092.MP4 -i 2018_0705_175815_093.MP4 -i 2018_0705_180115_094.MP4 -i 2018_0705_180415_095.MP4 -i 2018_0705_180715_096.MP4 -i 2018_0705_181015_097.MP4 -i 2018_0705_181315_098.MP4 -i 2018_0705_181615_099.MP4 -i 2018_0705_181915_100.MP4 -i 2018_0705_182215_101.MP4 -i 2018_0705_182515_102.MP4 -i 2018_0705_182815_103.MP4 -i 2018_0705_183115_104.MP4 -i 2018_0705_183415_105.MP4 -i 2018_0705_183715_106.MP4 -i 2018_0705_184015_107.MP4 -i 2018_0705_184315_108.MP4 -i 2018_0705_184615_109.MP4 -i 2018_0705_184915_110.MP4 -i 2018_0705_185215_111.MP4 -i 2018_0705_185515_112.MP4 -i 2018_0705_185815_113.MP4 -i 2018_0705_190115_114.MP4 -i 2018_0705_190415_115.MP4 -filter_complex [0:v][1:v][2:v][3:v][4:v][5:v][6:v][7:v][8:v][9:v][10:v][11:v][12:v][13:v][14:v][15:v][16:v][17:v][18:v][19:v][20:v][21:v][22:v][23:v][24:v][25:v][26:v][27:v][28:v][29:v][30:v][31:v][32:v][33:v][34:v][35:v][36:v][37:v][38:v][39:v][40:v][41:v][42:v][43:v][44:v][45:v][46:v][47:v][48:v][49:v][50:v][51:v][52:v][53:v][54:v][55:v][56:v][57:v][58:v][59:v][60:v][61:v][62:v][63:v][64:v][65:v][66:v][67:v][68:v][69:v][70:v][71:v][72:v][73:v][74:v][75:v][76:v][77:v][78:v][79:v][80:v][81:v][82:v][83:v][84:v][85:v][86:v][87:v][88:v][89:v][90:v][91:v][92:v][93:v][94:v][95:v][96:v][97:v][98:v][99:v][100:v][101:v][102:v][103:v][104:v][105:v][106:v][107:v][108:v][109:v]concat=n=110:v=1[v];[v]setpts=(1/60)*PTS,drawtext=fontfile=C\\:/WINDOWS/Fonts/consola.ttf:fontcolor=white:fontsize=80:y=main_h-text_h-50:x=50:text=WimsWorld[o] -map [o] -c:v libx265 -crf 23 -preset veryfast -movflags +faststart -bf 2 -g 15 -pix_fmt yuv420p -y “Output.mp4”

Here’s an example of the command where all the input files are defined in the temporary file:

ffmpeg.exe -report -f concat -safe 0 -i C:\Users\Wim\AppData\Local\Temp\Wim4BD3.tmp -vf setpts=(1/60)*PTS,drawtext=fontfile=C\\:/WINDOWS/Fonts/consola.ttf:fontcolor=white:fontsize=80:y=main_h-text_h-50:x=50:text=WimsWorld -an -c:v libx265 -crf 23 -preset veryfast -movflags +faststart -bf 2 -g 15 -pix_fmt yuv420p -y “Output.mp4”


FFMPEG and ROAV Dash Cam C1 Pro

I recently purchased a dedicated dashcam on sale to replace my GoPro setup for trip videos. This gives me a new need to understand a new file format.


The Roav Dashcam stores sequential mp4 files. When configuring the camera it’s possible to set the loop time, which is the duration of each mp4. There’s also an option to watermark the files. I have it turned on, and the only thing I’ve noticed is the ROAV logo, timestamp, and speed in the bottom right. It does not appear to have a way of adjusting the size of the text.

My initial recordings were set to run at 1080p 60 fps. I wanted to concatenate multiple files, add some text of my own, and speed up the video. This was my first experience using the -filter_complex option of FFMPEG. Here’s what I came up with to put together three files, speed the output up by a factor of 60, and add some text. I’m dropping the audio completely. The ROAV can record audio inside the car, but I configured it not to, as I don’t want to hear what I was listening to on the radio or what I might be saying if I make a phone call..

ffmpeg.exe -hide_banner -i 2018_0512_130537_050A.MP4 -i 2018_0512_131537_051A.MP4 -i 2018_0512_132537_052A.MP4 -filter_complex "[0:v] [1:v] [2:v] concat=n=3:v=1 [v];[v]setpts=0.01666*PTS,drawtext=fontfile=C\\:/WINDOWS/Fonts/consola.ttf:fontcolor=white:fontsize=80:y=main_h-text_h-50:x=50:text=WimsWorld[o]" -map "[o]" -c:v libx265 -crf 23 -preset veryfast -movflags +faststart -bf 2 -g 15 -pix_fmt yuv420p  FirstMixSpeed60Concat.mp4

This first video was recorded at 1080p60. The camera can record at 1440p30 which I will be trying soon to see if things like license plates are more legible. The setpts factor that I’m currently using was 1/60, so that 1 minute of real time was compressed to 1 second of video, and just dropping the extra frames. I expect to need to change the setpts factor to 1/30 because of the decreased frame rate at the higher resolution.

FFMPEG and h.265

I’ve noticed that YouTube transcodes my videos after I upload them and wanted to know more. It turns out that they are internally using a form of h.265 video encoding, which reduces the data size significantly without reducing perceived quality over h.264 video compression.

I decided to run some tests using my GoPro time lapse program to see how much compression I’d get versus how much extra time for encoding.

First I had to read up on the settings for using h.265 in FFmpeg. According to, If I add -c:v libx265 into my existing FFmpeg command line without changing anything, I’ll get an h.265 output with the defaults of -crf 23 and -preset medium.

The -preset value effects how much work is done in the compression, but shouldn’t affect the perceived quality. It will effect both time to create and output file size.

The -crf value effects the perceived quality. I’ve been using the defaults in my previous h.264 mp4 files, which should be approximately -crf 23 and supposedly -crf 28 in h.265 is equivalent to the lower h.264 value. A -crf 0 would be a completely lossless conversion. For my tests, I left crf at the default 23.

I ran all of these tests on a set of 7,129 photos I’d captured while sailboat racing on March 24th using my GoPro Hero 3+ Black. Each input image was 4000×3000 and I’m creating an output video with resolution 3840×2160 by cropping it at 3/4 of the height and scaling to fit.

The original h.264 conversion took 28:36 minutes to create and was 1,162,079,866 bytes long.

Here’s a trimmed down and sorted file listing. The first column is the time it took to create, second is filesize, and third is filename.

28:36 1,162,079,866 20180324-2160p30-cropped-h264.mp4
30:25 1,006,292,960 20180324-veryfast-2160p30-cropped-h265-crf20.mp4
21:35   328,700,100 20180324-ultrafast-2160p30-cropped-crf28.mp4
21:59   339,438,778 20180324-superfast-2160p30-cropped-crf28.mp4
25:12   350,085,434 20180324-veryfast-2160p30-cropped-crf28.mp4
25:17   349,720,030 20180324-faster-2160p30-cropped-crf28.mp4
27:24   348,582,310 20180324-fast-2160p30-cropped-crf28.mp4
52:07   365,050,252 20180324-medium-2160p30-cropped-crf28.mp4

I created a single test at -crf 20 because I was interested in seeing a higher quality video and how different it would be in size. It took slightly longer than the original h.264 compression and had a slight improvement in size.

Two things became obvious to me from this test. More time spent in compression doesn’t always mean better compression. For my application, running preset medium actually hurts the performance, both in file size and time taken.

I ran all of these tests only a single time on my desktop computer with an Intel(R) Core(TM) i7-4771 CPU @ 3.50GHz, 3501 Mhz, 4 Core(s), 8 Logical Processor(s) running the latest version of Windows Version 10.0.16299 Build 16299. I was running FFmpeg version N-88668-g723b6baaf8 from The variations in time could be related to background tasks running on the machine, and I should have run a more comprehensive battery of tests, averaging time and with more accurate timekeeping.

Odd Wildcard Matching in Windows 10

I recently ran into an odd behavior of more files matching a pattern than I expected. I’d used exiftool to modify the dates on files my GoPro produced. It creates backup files of the original images when it modifies the tags. Here’s the command I ran.

exiftool.exe -r "-AllDates+=4:7:6 17:40:00" -ext jpg f:\GoPro\20170807

Now I had about 4000 files with the .JPG extension and another 4000 files with a .JPG_original extension.

I ran my program that parses the directory structure and turns all those images into a time lapse movie, and it seemed to be including both the file extensions, making a very disjointed movie.

I loaded my source code in the debugger and it seemed to be doing a findfirst / findnext specifically looking for .JPG files, and not some other extension, but it was definitely retrieving files both with .JPG and .JPG_original extensions.

I then ran a couple of commands at the windows command prompt and was surprised to find the same results there.

dir F:\GoPro\20170807\372GOPRO\G*.JPG /p
dir F:\GoPro\20170807\372GOPRO\G???????.JPG /p

Each command returned both the JPG and JPG_original files.

dir F:\GoPro\20170807\372GOPRO\G*.JPG_original /p

returned just the JPG_original files.

dir F:\GoPro\20170807\372GOPRO\G??????.JPG /p

had one less question mark and correctly returned no files.

This is all unexpected behavior, though I’m glad to see that it was consistent with the operating system and not something specific to the C runtime. I’d love an explanation of what’s going on.

exiftool to manage DJI media files

DJI Drones don’t seem to remember the image count between formats of a media card. This creates a problem for me when I’m trying to backup and maintain my images and video.

Because the dates are all correct in the media files, retrieved from GPS data, organizing the files by naming them based on the date works for me.

Using ExifTool by Phil Harvey is a great solution for pulling the metadata from the files and renaming the files.

The command line that I was initially using is:

exiftool "-FileName<${CreateDate}.$filetype" -d %Y%m%d-%H%M%S%%-c -ext mp4 -ext dng -ext jpg dji*

It’s problem is that it orphans the SRT subtitle files from my videos that I’d like to keep matching the video files.

I’ve tried this variation to do it in one step but it doesn’t work, because the SRT files get renamed as MP4 files.

exiftool -verbose "-FileName<${CreateDate}" -d %Y%m%d-%H%M%S%%-c.%%le -ext mp4 -ext dng -ext jpg dji* -srcfile

If anyone has a suggestion for how to rename all the media files in one directory I’d appreciate it. Even running two commands in sequence would be fine.


I’ve figured out that running these two commands in sequence will get me the results I am looking for:

exiftool "-FileName<${CreateDate}" -d -ext mp4 -srcfile dji*
exiftool "-FileName<${CreateDate}" -d %Y%m%d-%H%M%S%%-c.%%le -ext mp4 -ext dng -ext jpg dji*

I’m still looking for a way of doing it in a single command that may leave less room for error, but this is working for now.

FFMPEG and drawtext

Several years ago I wrote a program that consolidates time-lapse pictures into a directory and calls FFMPEG to create a video.

I had been wanting the time-code from when each picture was taken printed on the screen while the video was playing but had not figured out how to get it done until this weekend.

Video TimeCode

Frame from video showing the DateTimeOriginal timecode embedded.

I’d gone down multiple paths in an attempt to get this result before finally getting the drawtext feature to work. My program manually pulled the metadata from the images before feeding them to ffmpg. I’d tried creating both text files and image files for overlaying. none of those got the result that I was looking for.

When I finally got everything working, it seems simple, but the underlying problem has to do with the amount of string escaping required to get the command to work.

Here’s an example command I was issuing to ffmpeg that got the result I was looking for.

ffmpeg.exe -hide_banner -r 30 -i Wim%05d.JPG -vf crop=in_w:3/4*in_h,drawtext=fontfile=C\\:/WINDOWS/Fonts/OCRAEXT.ttf:fontcolor=white:fontsize=160:y=main_h-text_h-50:x=main_w-text_w-50:text=WimsWorld,drawtext=fontfile=C\\:/WINDOWS/Fonts/OCRAEXT.ttf:fontcolor=white:fontsize=160:y=main_h-text_h-50:x=50:text=%{metadata\\:DateTimeOriginal} -s 3840x2160 -pix_fmt yuv420p -n Test-2160p30-cropped.mp4

If you look at the -vf option parameter, I’m cropping my input pictures to 3/4 their original height, then using the drawtext feature twice. First I write the static text to the bottom right of the frame, then I extract metadata from the source image and write it to the bottom left of the frame.

Because I’m calling this from a program, I had extra escaping of the \ character in my code. All of the escaping required a lot of trial and error to get things working. I’m using OCRAEXT as my font, but I could be using any fixed spacing font. because of the fact that the time is changing every frame, it’s important that the font not be proportional to make it easy to read.