Repository: mapillary/mapillary_tools

Issue:
Feature request: GPS info from Viofo A139 Pro dashcam videos
<!--
   Thanks for contributing! Request a feature or use the
   template below for reporting bugs.

   To help us diagnose problems, please:

  - Check the console for relevant errors and warnings.
  
  - Provide a link to where the problem occurs (if applicable)

  - Attach screenshots and logs if possible/applicable
  
  - Remove template lines which do not apply to your issue/feature request
-->

### Basic information
* Release version: 0.10.1
* System: `Windows`
* Capture Device: `Viofo A139 Pro 2Ch` (dashcam 4k+1k)

### Steps to reproduce behavior

 1. Process video: `python3.exe -m mapillary_tools.commands video_process  --skip_process_errors --interpolate_directions .\2023_0516_192739_F.MP4`
 2. Verify presence of GPS data: `exiftool.exe  -p 'C:\Program Files\exiftool\gpx.fmt' -api largefilesupport=1 -ee -ext MP4 -w 2023_0516_192739_F.gpx .\2023_0516_192739_F.MP4
`
### Expected behavior

1. The video_process  sub-command would extract images with GPS latitude/longitude info.
2. exiftool do generate uabel .gpx file.

### Actual behavior

1.  Processing failure, as the GPS info ins not found ("missing" feature):

```
> python3.exe -m mapillary_tools.commands video_process  --skip_process_errors --interpolate_directions .\2023_0516_192739_F.MP4
2023-05-20 10:45:36,078 - INFO    - Extracting video information: ffprobe -print_format json -hide_banner -show_format -show_streams 2023_0516_192739_F.MP4
Input #0, mov,mp4,m4a,3gp,3g2,mj2, from '2023_0516_192739_F.MP4':
  Metadata:
    major_brand     : mp42
    minor_version   : 0
    compatible_brands: isomavc1mp42
    creation_time   : 2023-05-16T18:27:39.000000Z
  Duration: 00:00:33.63, start: 0.000000, bitrate: 29720 kb/s
  Stream #0:0[0x1](eng): Video: h264 (High) (avc1 / 0x31637661), yuvj420p(pc, bt709, progressive), 3840x2160, 29458 kb/s, 30 fps, 30 tbr, 60k tbn (default)
    Metadata:
      creation_time   : 2023-05-16T19:33:05.000000Z
      handler_name    : VideoHandler
      vendor_id       : [0][0][0][0]
      encoder         : h264
2023-05-20 10:45:36,211 - INFO    - Extracting video metdata
2023-05-20 10:45:36,214 - **WARNING - No GPS data found from the video**
2023-05-20 10:45:36,214 - WARNING - Force the option "filetypes" to be "image" to avoid processing and uploading both the video samples and the videos themselves
2023-05-20 10:45:36,222 - ERROR   - MapillaryFileNotFoundError: Import file or directory not found: C:\Users\luser\Downloads\mapillary_sampled_video_frames

```


2. Verification of GPS data does succeed, and while there are issues, these should not necessarily blocking processing:

```
Warning: [Minor] Tag 'Main:gpsaltitude' not defined - ./2023_0516_192739_F.MP4
Warning: [Minor] Tag 'Main:gpsdatetime' not defined - ./2023_0516_192739_F.MP4
Warning: [Minor] Tag 'Doc9:gpsaltitude' not defined - ./2023_0516_192739_F.MP4
Warning: [Minor] Tag 'Doc10:gpsaltitude' not defined - ./2023_0516_192739_F.MP4
Warning: [Minor] Tag 'Doc11:gpsaltitude' not defined - ./2023_0516_192739_F.MP4
Warning: [Minor] Tag 'Doc12:gpsaltitude' not defined - ./2023_0516_192739_F.MP4
Warning: [Minor] Tag 'Doc13:gpsaltitude' not defined - ./2023_0516_192739_F.MP4
Warning: [Minor] Tag 'Doc14:gpsaltitude' not defined - ./2023_0516_192739_F.MP4
Warning: [Minor] Tag 'Doc15:gpsaltitude' not defined - ./2023_0516_192739_F.MP4
Warning: [Minor] Tag 'Doc16:gpsaltitude' not defined - ./2023_0516_192739_F.MP4
Warning: [Minor] Tag 'Doc17:gpsaltitude' not defined - ./2023_0516_192739_F.MP4
Warning: [Minor] Tag 'Doc18:gpsaltitude' not defined - ./2023_0516_192739_F.MP4
Warning: [Minor] Tag 'Doc1:gpsaltitude' not defined - ./2023_0516_192739_F.MP4
Warning: [Minor] Tag 'Doc19:gpsaltitude' not defined - ./2023_0516_192739_F.MP4
Warning: [Minor] Tag 'Doc20:gpsaltitude' not defined - ./2023_0516_192739_F.MP4
Warning: [Minor] Tag 'Doc21:gpsaltitude' not defined - ./2023_0516_192739_F.MP4
Warning: [Minor] Tag 'Doc22:gpsaltitude' not defined - ./2023_0516_192739_F.MP4
Warning: [Minor] Tag 'Doc23:gpsaltitude' not defined - ./2023_0516_192739_F.MP4
Warning: [Minor] Tag 'Doc24:gpsaltitude' not defined - ./2023_0516_192739_F.MP4
Warning: [Minor] Tag 'Doc25:gpsaltitude' not defined - ./2023_0516_192739_F.MP4
Warning: [Minor] Tag 'Doc26:gpsaltitude' not defined - ./2023_0516_192739_F.MP4
Warning: [Minor] Tag 'Doc27:gpsaltitude' not defined - ./2023_0516_192739_F.MP4
Warning: [Minor] Tag 'Doc28:gpsaltitude' not defined - ./2023_0516_192739_F.MP4
Warning: [Minor] Tag 'Doc2:gpsaltitude' not defined - ./2023_0516_192739_F.MP4
Warning: [Minor] Tag 'Doc29:gpsaltitude' not defined - ./2023_0516_192739_F.MP4
Warning: [Minor] Tag 'Doc30:gpsaltitude' not defined - ./2023_0516_192739_F.MP4
Warning: [Minor] Tag 'Doc31:gpsaltitude' not defined - ./2023_0516_192739_F.MP4
Warning: [Minor] Tag 'Doc32:gpsaltitude' not defined - ./2023_0516_192739_F.MP4
Warning: [Minor] Tag 'Doc33:gpsaltitude' not defined - ./2023_0516_192739_F.MP4
Warning: [Minor] Tag 'Doc3:gpsaltitude' not defined - ./2023_0516_192739_F.MP4
Warning: [Minor] Tag 'Doc4:gpsaltitude' not defined - ./2023_0516_192739_F.MP4
Warning: [Minor] Tag 'Doc5:gpsaltitude' not defined - ./2023_0516_192739_F.MP4
Warning: [Minor] Tag 'Doc6:gpsaltitude' not defined - ./2023_0516_192739_F.MP4
Warning: [Minor] Tag 'Doc7:gpsaltitude' not defined - ./2023_0516_192739_F.MP4
Warning: [Minor] Tag 'Doc8:gpsaltitude' not defined - ./2023_0516_192739_F.MP4
    1 output files created
```




### Corresponding data

* sample video: https://drive.google.com/file/d/1E_ecXTVyG9qvUxZDS60gYIK2y9o0oLU2/view?usp=sharing
* GPS meta data: https://drive.google.com/file/d/1NZ23hwyNOZ1xjIha0yn4hHt2fdK2v2v5/view?usp=sharing



### Additional information

... 

<!-- screenshots, system logs etc. -->

Discussion:
@stefanbj: I have successfully processes a video, after a bit of manipulation of the .MP4 files metadata using exiftool and light editing of the extracted .gpx file; but this a bit too error prone and time consuming for regular uploads.@ptpt: @stefanbj thanks for providing the sample videos. We are planning to integrate exiftool and mapillary_tools to ease the processing for the other video formats.

> after a bit of manipulation of the .MP4 files metadata using exiftool and light editing of the extracted .gpx file

Could you elaborate how did you manipulate the video files?@stefanbj: Sorry, it was very late when I opened the case, and I don't recall what the exactly issues was, but it likely had something correlating the timestamps; could not reproduce the issue today.

### Some observations

1. While I do like exiftool, not everyone have it installed, and it isn't exactly the fastest at processing the video metadata and generating the the .gpx file. Thus I would advise against making it a requirement, but as a fallback it would likely be a great improvement.

2. exiftool is good, but the current version seems to have a few bugs.

2.1.  I believe the first trackpoint in the .gpx file is extracted for the .MP4 header (if available), and while the position seems to be OK, the timestamp never seem to be usable.
Specifically for Ricoh Theta X videos, it in local time, that is NOT synchronized with the GPS time for the rest of the trackpoints, thus it useless. (The first trackpoint can be several minutes before the regular 1 second interval gps positions start at the second trackpoint, and G.S.V. refuses imports with this issue.)  
The workaround seems to he to hardcode this to be 1 second before the second before the second trackpoint, again specifically this works fine for Ricoh Theta X. 

This is not an issue for the Viofo A139 Pro dashcam videos, as these do have a GPS position in the .MP4 header.
Unfortunately not all videos have a 1 second GPS intervall, so probably safer to instead just the interval between the second and third trackpoint; specifically GPS interval seems to be configurable on the Viofo dashcams.

2.2. I believe the .gpx file is expected to be in local time, and thus most SW seems to ignore the 'Z' Zulu/Greenwich time zone indication in the timestamps that exiftool adds.
(Specifically both G.S.V.  has issues with both issues, so .gpx needs to be corrected for both before uploads of videos cut by ffmpeg (e.g. without camm data).)
This seems to be an issue with mapillary_tools, as there doesn't seem to be any safe generic way to correlate the absolute times of the video file and .gpx file, a workaround is needed. 
Would suggest assuming that the video files local time timestamp is within 15 minutes of the correct local time, and then calculate the offset to the second trackpoint in the .gpx file, round this down to even 30 min offset, and then use that offset for any conversion between local time and GPS time in the .gpx file.

2. Check the interval of the GPS track positions -- and complain loudly if it more then 1 second apart -- if so, it's most likely a configuration in the dashcam fault that user need correct for (power saving feature).

3. The video file my not have a full set of timestamps, i.e. both local time and GPS time; specifically the Viofo A139 Pro dashcam videos seems to be missing most of the usual metadata attributes in the file header, including the GPS time stamp. 
So probably safest to verify that the .gpx track has the same length in time as the video file, and ignore the absolute values of time-stamps entirely, and consider instead to do the processing using relative 


### Recipe
1. Extract the .gpx data, eg.: `exiftool -p 'C:\Program Files\exiftool\gpx.fmt' -api largefilesupport=1 -ee -ext MP4 -w %f/%f.gpx  *.MP4`

Example 2:
```
PS J:\Dashcam\Movie> exiftool.exe  -p 'C:\Program Files\exiftool\gpx.fmt' -api largefilesupport=1 -ee -ext MP4 -w 2023_0527_134513_F.gpx 2023_0527_134513_F.MP4
Warning: [Minor] Tag 'Doc1:gpsaltitude' not defined - 2023_0527_134513_F.MP4
Warning: [Minor] Tag 'Doc2:gpsaltitude' not defined - 2023_0527_134513_F.MP4
Warning: [Minor] Tag 'Doc11:gpsaltitude' not defined - 2023_0527_134513_F.MP4
Warning: [Minor] Tag 'Doc12:gpsaltitude' not defined - 2023_0527_134513_F.MP4
Warning: [Minor] Tag 'Doc13:gpsaltitude' not defined - 2023_0527_134513_F.MP4
Warning: [Minor] Tag 'Doc14:gpsaltitude' not defined - 2023_0527_134513_F.MP4
Warning: [Minor] Tag 'Doc15:gpsaltitude' not defined - 2023_0527_134513_F.MP4
Warning: [Minor] Tag 'Doc16:gpsaltitude' not defined - 2023_0527_134513_F.MP4
Warning: [Minor] Tag 'Doc17:gpsaltitude' not defined - 2023_0527_134513_F.MP4
Warning: [Minor] Tag 'Doc18:gpsaltitude' not defined - 2023_0527_134513_F.MP4
Warning: [Minor] Tag 'Doc19:gpsaltitude' not defined - 2023_0527_134513_F.MP4
Warning: [Minor] Tag 'Doc20:gpsaltitude' not defined - 2023_0527_134513_F.MP4
Warning: [Minor] Tag 'Doc3:gpsaltitude' not defined - 2023_0527_134513_F.MP4
Warning: [Minor] Tag 'Doc21:gpsaltitude' not defined - 2023_0527_134513_F.MP4
Warning: [Minor] Tag 'Doc22:gpsaltitude' not defined - 2023_0527_134513_F.MP4
Warning: [Minor] Tag 'Doc23:gpsaltitude' not defined - 2023_0527_134513_F.MP4
Warning: [Minor] Tag 'Doc24:gpsaltitude' not defined - 2023_0527_134513_F.MP4
Warning: [Minor] Tag 'Doc25:gpsaltitude' not defined - 2023_0527_134513_F.MP4
Warning: [Minor] Tag 'Doc26:gpsaltitude' not defined - 2023_0527_134513_F.MP4
Warning: [Minor] Tag 'Doc27:gpsaltitude' not defined - 2023_0527_134513_F.MP4
Warning: [Minor] Tag 'Doc28:gpsaltitude' not defined - 2023_0527_134513_F.MP4
Warning: [Minor] Tag 'Doc29:gpsaltitude' not defined - 2023_0527_134513_F.MP4
Warning: [Minor] Tag 'Doc30:gpsaltitude' not defined - 2023_0527_134513_F.MP4
Warning: [Minor] Tag 'Doc4:gpsaltitude' not defined - 2023_0527_134513_F.MP4
Warning: [Minor] Tag 'Doc31:gpsaltitude' not defined - 2023_0527_134513_F.MP4
Warning: [Minor] Tag 'Doc32:gpsaltitude' not defined - 2023_0527_134513_F.MP4
Warning: [Minor] Tag 'Doc33:gpsaltitude' not defined - 2023_0527_134513_F.MP4
Warning: [Minor] Tag 'Doc34:gpsaltitude' not defined - 2023_0527_134513_F.MP4
Warning: [Minor] Tag 'Doc35:gpsaltitude' not defined - 2023_0527_134513_F.MP4
Warning: [Minor] Tag 'Doc36:gpsaltitude' not defined - 2023_0527_134513_F.MP4
Warning: [Minor] Tag 'Doc37:gpsaltitude' not defined - 2023_0527_134513_F.MP4
Warning: [Minor] Tag 'Doc38:gpsaltitude' not defined - 2023_0527_134513_F.MP4
Warning: [Minor] Tag 'Doc39:gpsaltitude' not defined - 2023_0527_134513_F.MP4
Warning: [Minor] Tag 'Doc40:gpsaltitude' not defined - 2023_0527_134513_F.MP4
Warning: [Minor] Tag 'Doc5:gpsaltitude' not defined - 2023_0527_134513_F.MP4
Warning: [Minor] Tag 'Doc41:gpsaltitude' not defined - 2023_0527_134513_F.MP4
Warning: [Minor] Tag 'Doc42:gpsaltitude' not defined - 2023_0527_134513_F.MP4
Warning: [Minor] Tag 'Doc43:gpsaltitude' not defined - 2023_0527_134513_F.MP4
Warning: [Minor] Tag 'Doc6:gpsaltitude' not defined - 2023_0527_134513_F.MP4
Warning: [Minor] Tag 'Doc7:gpsaltitude' not defined - 2023_0527_134513_F.MP4
Warning: [Minor] Tag 'Doc8:gpsaltitude' not defined - 2023_0527_134513_F.MP4
Warning: [Minor] Tag 'Doc9:gpsaltitude' not defined - 2023_0527_134513_F.MP4
Warning: [Minor] Tag 'Doc10:gpsaltitude' not defined - 2023_0527_134513_F.MP4
    1 output files created
PS J:\Dashcam\Movie>

```
 
2. Edit and convert the .gpx file to local time; In my case adding 1 hour to the timestamps to convert from Zulu to CET TZ seems to work.

3. Process the video:

```
PS J:\Dashcam\Movie> python3.exe -m mapillary_tools.commands --verbose video_process --rerun  --video_sample_distance -1 --video_sample_interval 1 --duplicate_distance 3 --overwrite_all_EXIF_tags  --interpolate_directions --skip_process_errors --geotag_source gpx --geotag_source_path  .\2023_0527_111351_F\2023_0527_111351_F_d2.gpx .\2023_0527_111351_F.MP4
2023-05-27 20:58:15,116 - DEBUG   - mapillary_tools version 0.10.0.git
2023-05-27 20:58:15,117 - DEBUG   - CLI param: verbose: True
2023-05-27 20:58:15,117 - DEBUG   - CLI param: video_import_path: 2023_0527_111351_F.MP4
2023-05-27 20:58:15,118 - DEBUG   - CLI param: video_sample_distance: -1.0
2023-05-27 20:58:15,118 - DEBUG   - CLI param: video_sample_interval: 1.0
2023-05-27 20:58:15,118 - DEBUG   - CLI param: video_duration_ratio: 1.0
2023-05-27 20:58:15,118 - DEBUG   - CLI param: skip_subfolders: False
2023-05-27 20:58:15,118 - DEBUG   - CLI param: rerun: True
2023-05-27 20:58:15,118 - DEBUG   - CLI param: skip_sample_errors: False
2023-05-27 20:58:15,118 - DEBUG   - CLI param: skip_process_errors: True
2023-05-27 20:58:15,118 - DEBUG   - CLI param: filetypes: image, gopro, camm, blackvue
2023-05-27 20:58:15,118 - DEBUG   - CLI param: overwrite_all_EXIF_tags: True
2023-05-27 20:58:15,119 - DEBUG   - CLI param: overwrite_EXIF_time_tag: False
2023-05-27 20:58:15,119 - DEBUG   - CLI param: overwrite_EXIF_gps_tag: False
2023-05-27 20:58:15,119 - DEBUG   - CLI param: overwrite_EXIF_direction_tag: False
2023-05-27 20:58:15,119 - DEBUG   - CLI param: overwrite_EXIF_orientation_tag: False
2023-05-27 20:58:15,119 - DEBUG   - CLI param: add_file_name: False
2023-05-27 20:58:15,119 - DEBUG   - CLI param: add_import_date: False
2023-05-27 20:58:15,119 - DEBUG   - CLI param: geotag_source: gpx
2023-05-27 20:58:15,119 - DEBUG   - CLI param: geotag_source_path: 2023_0527_111351_F\2023_0527_111351_F_d2.gpx
2023-05-27 20:58:15,119 - DEBUG   - CLI param: interpolation_use_gpx_start_time: False
2023-05-27 20:58:15,120 - DEBUG   - CLI param: interpolation_offset_time: 0.0
2023-05-27 20:58:15,120 - DEBUG   - CLI param: offset_angle: 0.0
2023-05-27 20:58:15,120 - DEBUG   - CLI param: offset_time: 0.0
2023-05-27 20:58:15,120 - DEBUG   - CLI param: cutoff_distance: 600.0
2023-05-27 20:58:15,120 - DEBUG   - CLI param: cutoff_time: 60.0
2023-05-27 20:58:15,120 - DEBUG   - CLI param: interpolate_directions: True
2023-05-27 20:58:15,120 - DEBUG   - CLI param: duplicate_distance: 3.0
2023-05-27 20:58:15,120 - DEBUG   - CLI param: duplicate_angle: 5.0
2023-05-27 20:58:15,123 - INFO    - Removing the sample directory J:\Dashcam\Movie\mapillary_sampled_video_frames\2023_0527_111351_F.MP4
2023-05-27 20:58:15,191 - INFO    - Extracting video information: ffprobe -print_format json -hide_banner -show_format -show_streams 2023_0527_111351_F.MP4
Input #0, mov,mp4,m4a,3gp,3g2,mj2, from '2023_0527_111351_F.MP4':
  Metadata:
    major_brand     : mp42
    minor_version   : 0
    compatible_brands: isomavc1mp42
    creation_time   : 2023-05-27T11:59:47.000000Z
  Duration: 00:01:32.40, start: 0.000000, bitrate: 29580 kb/s
  Stream #0:0[0x1](eng): Video: h264 (High) (avc1 / 0x31637661), yuvj420p(pc, bt709, progressive), 3840x2160, 29358 kb/s, 30 fps, 30 tbr, 60k tbn (default)
    Metadata:
      creation_time   : 2023-05-27T11:59:47.000000Z
      handler_name    : VideoHandler
      vendor_id       : [0][0][0][0]
      encoder         : h264
2023-05-27 20:58:15,332 - DEBUG   - Extracted video duration: 92.400000
2023-05-27 20:58:15,332 - DEBUG   - Extracted video creation time: 2023-05-27T11:59:47.000000Z
2023-05-27 20:58:15,337 - INFO    - Extracting frames: ffmpeg -hide_banner -nostdin -i 2023_0527_111351_F.MP4 -vf fps=1/1.0 -qscale:v 2 J:\Dashcam\Movie\mapillary_sampled_video_frames\.mly_ffmpeg_2023_0527_111351_F.MP4_1992_1685213895\2023_0527_111351_F_NA_%06d.jpg
Input #0, mov,mp4,m4a,3gp,3g2,mj2, from '2023_0527_111351_F.MP4':
  Metadata:
    major_brand     : mp42
    minor_version   : 0
    compatible_brands: isomavc1mp42
    creation_time   : 2023-05-27T11:59:47.000000Z
  Duration: 00:01:32.40, start: 0.000000, bitrate: 29580 kb/s
  Stream #0:0[0x1](eng): Video: h264 (High) (avc1 / 0x31637661), yuvj420p(pc, bt709, progressive), 3840x2160, 29358 kb/s, 30 fps, 30 tbr, 60k tbn (default)
    Metadata:
      creation_time   : 2023-05-27T11:59:47.000000Z
      handler_name    : VideoHandler
      vendor_id       : [0][0][0][0]
      encoder         : h264
Stream mapping:
  Stream #0:0 -> #0:0 (h264 (native) -> mjpeg (native))
Output #0, image2, to 'J:\Dashcam\Movie\mapillary_sampled_video_frames\.mly_ffmpeg_2023_0527_111351_F.MP4_1992_1685213895\2023_0527_111351_F_NA_%06d.jpg':
  Metadata:
    major_brand     : mp42
    minor_version   : 0
    compatible_brands: isomavc1mp42
    encoder         : Lavf59.27.100
  Stream #0:0(eng): Video: mjpeg, yuvj420p(pc, bt709, progressive), 3840x2160, q=2-31, 200 kb/s, 1 fps, 1 tbn (default)
    Metadata:
      creation_time   : 2023-05-27T11:59:47.000000Z
      handler_name    : VideoHandler
      vendor_id       : [0][0][0][0]
      encoder         : Lavc59.37.100 mjpeg
    Side data:
      cpb: bitrate max/min/avg: 0/0/200000 buffer size: 0 vbv_delay: N/A
frame=   92 fps=4.2 q=2.0 Lsize=N/A time=00:01:32.00 bitrate=N/A speed=4.24x
video:98721kB audio:0kB subtitle:0kB other streams:0kB global headers:0kB muxing overhead: unknown
2023-05-27 20:58:38,091 - WARNING - Force the option "filetypes" to be "image" to avoid processing and uploading both the video samples and the videos themselves
2023-05-27 20:58:38,586 - DEBUG   - Final time offset for interpolation: 0.0
The image time is 0.6 seconds beyond the GPX end point
The image time is 1.6 seconds beyond the GPX end point
The image time is 2.6 seconds beyond the GPX end point
The image time is 3.6 seconds beyond the GPX end point
The image time is 4.6 seconds beyond the GPX end point
The image time is 5.6 seconds beyond the GPX end point
The image time is 6.6 seconds beyond the GPX end point
The image time is 7.6 seconds beyond the GPX end point
The image time is 8.6 seconds beyond the GPX end point
The image time is 9.6 seconds beyond the GPX end point
The image time is 10.6 seconds beyond the GPX end point
The image time is 11.6 seconds beyond the GPX end point
2023-05-27 20:58:38,756 - INFO    - Check the description file for details: J:\Dashcam\Movie\mapillary_sampled_video_frames\mapillary_image_description.json
2023-05-27 20:58:38,756 - INFO    -       92 image(s) read in total
2023-05-27 20:58:38,757 - INFO    -             4 image(s) are ready to be uploaded
2023-05-27 20:58:38,757 - WARNING -            12 image(s) skipped due to MapillaryOutsideGPXTrackError
2023-05-27 20:58:38,757 - WARNING -            76 image(s) skipped due to MapillaryDuplicationError
PS J:\Dashcam\Movie>
```@stefanbj: A longer example file, demonstrating the 1 minute GPS intervall issue:

- https://drive.google.com/file/d/1JV1Oj44AKheCXegxdXbAqu9vK71MIvxt/view?usp=sharing

This also seem to  be a image time offset problem, so all 42 images are 366 seconds beyond the GPX end point... 

Think this was accidentally captured in timelapse mode, in theory this should be fine as the velocity is ignored, but the infrequent GPS positions is an issue - a 1 minute interval isn't really useful for mapping at highway speeds...@stefanbj: A few more  (low bitrate) sample videos to [Viofo139](https://drive.google.com/drive/folders/1C7pXEULWw7uoMxL2FeOESHwNmhILSEdH?usp=sharing), in case this would be useful for verification.

- [2023_0530_054703_F_lb43_3840_2160.MP4](https://drive.google.com/file/d/1160fIdU0MgUZD-MlIz5IPMUcawFywHAd/view?usp=sharing) 319MB 0:01:00
- [2023_0530_054704_R_lb15_hd.MP4](https://drive.google.com/file/d/1wUnkteY5dd3a2qwFYqLaMUI2CylLIqmW/view?usp=sharing) 112MB 0:01:00
- [2023_0530_054822_F_lb43_3840_1600.MP4](https://drive.google.com/file/d/1kK6U70We3nCgic2L4lG0L36hGmG671D1/view?usp=sharing) 47MB 00:00:08
- [2023_0530_054823_R_lb15_hd.MP4](https://drive.google.com/file/d/1innWRBCx-W1l43GRjIgLuz6oHkcPiDXx/view?usp=sharing) 16MB 00:00:08
- [2023_0530_054958_F_lb19_2560_1440_60.MP4](https://drive.google.com/file/d/1cne5KrhGdE_XD0IXgxkCFvNm3tzvwqns/view?usp=sharing) 133MB 0:00:55
- [2023_0530_054959_R_lb14_hd.MP4](https://drive.google.com/file/d/1EdOOGGiejbdNsvOxoAYsOszfd2lnjski/view?usp=sharing) 93MB 0:00:55
- [2023_0530_055105_F_lb19_2560_1440.MP4](https://drive.google.com/file/d/1uo9S95qcMwH_VMeoGAHQy5j1L9c6x1Ev/view?usp=sharing) 143MB 0:01:00
- [2023_0530_055105_R_lb14_hd.MP4](https://drive.google.com/file/d/1LHrE-7btrT8SqRju4ytINwrpOYCF_SpU/view?usp=sharing) 103MB 0:01:00
- [2023_0530_055205_F_lb19_2560_1440.MP4](https://drive.google.com/file/d/1uXIFlIEAeXbN1JV1opd9PPqPHhynkpAk/view?usp=sharing) 17MB 00:00:07
- [2023_0530_055105_R_lb14_hd.MP4](https://drive.google.com/file/d/1TVCtCn6X8BxWH5I0VDkunKqwfhn35i35/view?usp=sharing) 12MB 00:00:07
- [2023_0530_055314_F_lb15_2560_1440.MP4](https://drive.google.com/file/d/1VceOwq3YCL8-HV_Wkra06-uRgfw7SwQa/view?usp=sharing) 47MB 00:00:25
- [2023_0530_055315_R_lb14_hd.MP4](https://drive.google.com/file/d/1Rh-FNhKJskvGiTmrcqvaHoqLBYC74y-u/view?usp=sharing) 31MB 00:00:25
- [2023_0530_055358_F_lb15_2560_1440_60.MP4](https://drive.google.com/file/d/1WMVBqUD7-igo8jrB9L3gBOCXxU1aVxUK/view?usp=sharing)  144MB 0:01:16
- [2023_0530_055359_R_lb9_hd.MP4](https://drive.google.com/file/d/1mUouhhUnnGK1ZGzSo2cN7ugbHlJXpisV/view?usp=sharing) 95MB 0:01:16
@stefanbj: Tried to see if I could find a way to generate and read the .gpx file, as a pathfinder, and this worked sort of, see:
 - [https://github.com/mapillary/mapillary_tools/compare/main...stefanbj:mapillary_tools:issue619](url)
However more work ins needed to address the timestamp differences etc, and the camera_model part is not implemented.

Also noted that some seemingly related work has been done in the feature branch:
- [feat-exiftool-parsing](https://github.com/mapillary/mapillary_tools/tree/feat-exiftool-parsing)
This seems to be much more comprehensive in scope though


Output example, from a test run:
```
$ python3.exe -d -m mapillary_tools.commands video_process --rerun K:/Dashcam/2023-06-01/Movie/RO/2023_0601_063550_F.MP4 2023_0601_063550_F
2023-06-02 19:38:20,337 - INFO    - Removing the sample directory 2023_0601_063550_F\2023_0601_063550_F.MP4
2023-06-02 19:38:20,338 - INFO    - Extracting video information: ffprobe -print_format json -hide_banner -show_format -show_streams K:\Dashcam\2023-06-01\Movie\RO\2023_0601_063550_F.MP4
Input #0, mov,mp4,m4a,3gp,3g2,mj2, from 'K:\Dashcam\2023-06-01\Movie\RO\2023_0601_063550_F.MP4':
[[...]]
2023-06-02 19:38:20,382 - INFO    - Extracting video metdata
2023-06-02 19:38:20,384 - INFO    - Attempting to extracting video metdata using exiftool
2023-06-02 19:38:20,384 - INFO    - C:extract_camera_model file : K:\Dashcam\2023-06-01\Movie\RO\2023_0601_063550_F.MP4
2023-06-02 19:38:20,384 - INFO    - Videofile as .gpx source : K:\Dashcam\2023-06-01\Movie\RO\2023_0601_063550_F.MP4
2023-06-02 19:38:20,385 - INFO    - File to create gpx : K:\Dashcam\2023-06-01\Movie\RO\2023_0601_063550_F.gpx.tmp
12.55
2023-06-02 19:38:20,781 - INFO    - The exiftool command OK: exiftool
2023-06-02 19:38:20,784 - INFO    - formatfile file is: 'C:\cygwin64\tmp\gpx.fmt_4x9090oy.tmp'
2023-06-02 19:38:20,873 - INFO    - Creating .GPX file : exiftool -p C:\cygwin64\tmp\gpx.fmt_4x9090oy.tmp -api largefilesupport=1 -ee -w %d/%f.gpx.tmp K:\Dashcam\2023-06-01\Movie\RO\2023_0601_063550
_F.MP4
Warning: [Minor] Tag 'Doc1:gpsaltitude' not defined - K:/Dashcam/2023-06-01/Movie/RO/2023_0601_063550_F.MP4
Warning: [Minor] Tag 'Doc2:gpsaltitude' not defined - K:/Dashcam/2023-06-01/Movie/RO/2023_0601_063550_F.MP4
Warning: [Minor] Tag 'Doc11:gpsaltitude' not defined - K:/Dashcam/2023-06-01/Movie/RO/2023_0601_063550_F.MP4
Warning: [Minor] Tag 'Doc101:gpsaltitude' not defined - K:/Dashcam/2023-06-01/Movie/RO/2023_0601_063550_F.MP4
[[...]]
Warning: [Minor] Tag 'Doc100:gpsaltitude' not defined - K:/Dashcam/2023-06-01/Movie/RO/2023_0601_063550_F.MP4
    1 output files created
2023-06-02 19:38:23,503 - INFO    - Found total 230 GPS points
2023-06-02 19:38:23,503 - INFO    - Extracting video samples
2023-06-02 19:38:23,505 - INFO    - Extracting video samples
2023-06-02 19:38:23,554 - INFO    - Found total 6913 video samples
2023-06-02 19:38:23,554 - INFO    - Interpolating video samples in the time range from 1685597748.0 to 1685597977.0
2023-06-02 19:38:23,557 - INFO    - Found total 0 interpolated video samples
2023-06-02 19:38:23,557 - INFO    - Selected 0 video samples by the minimal sample distance 3.0
2023-06-02 19:38:23,562 - WARNING - Force the option "filetypes" to be "image" to avoid processing and uploading both the video samples and the videos themselves
Extracting geotags from images: 0images [00:00, ?images/s]
2023-06-02 19:38:23,779 - INFO    - Validating 0 metadatas
2023-06-02 19:38:23,779 - INFO    - Checking upload status for 0 metadatas
2023-06-02 19:38:23,780 - INFO    - Check the description file for details: 2023_0601_063550_F\mapillary_image_description.json

````@ptpt: Thanks for providing all the useful samples and information.

The branch [feat-exiftool-parsing](https://github.com/mapillary/mapillary_tools/tree/feat-exiftool-parsing) is mainly focus on parsing exiftool XML outuput for images.

For videos, it's coming soon in another branch. Hopefully, you will process all images/videos with this workflow

```shell
exiftool -X -n -ee -r -api LargeFileSupport=1 videos/ > exiftool_output.xml

# local sampling
mapillary_tools video_process --geotag_source=exiftool --geotag_source_path=exiftool_output.xml videos/

# or directly process and upload
mapillary_tools process_and_upload --geotag_source=exiftool --geotag_source_path=exiftool_output.xml videos/
```

Let me know if it makes things easier.@ptpt: BTW You can manually align the video sample timestamps and the GPS track timestamps with these parameters in the process command:
```
  --interpolation_use_gpx_start_time
                        If supplied, the first image will use the first GPX point time for interpolation, which means the image location will be interpolated to the first GPX point too. Only works for geotagging from gpx, gopro_videos, nmea, blackvue_videos, camm.
  --interpolation_offset_time INTERPOLATION_OFFSET_TIME
                        Time offset, in seconds, that will be added for GPX interpolation, which affects image locations. Note that it is applied after --interpolation_use_gpx_start_time. Only works for geotagging from gpx, gopro_videos, nmea, blackvue_videos, camm. [default 0.0]
```@stefanbj: I'm sure I could make that work; in fact I think it would be very good to have that option, as I have found a few videos where the recorded orientation most likely isn't correct - and this would likely make it easier to resolve/workaround that issue.

However feel equally sure most users would prefer a solution that can be more easily memorized, and doesn't require copy&pasting from the manual each time they start over. Unfortunately many user tend to give up if things don't work more or less immediately out of the box, e.g. people that have just figured out that the [Desktop uploader](https://help.mapillary.com/hc/en-us/articles/360020825811) does not work for their use case... (As a "former" engineer, I tend to be surprised by how much ease of use matters in practice, and how much that changes from person to person and thus the amount of support needed.)
@ptpt: Try out the latest alpha https://github.com/mapillary/mapillary_tools/releases/tag/v0.10.3a


Here is the workflow:

```sh
exiftool -r -n -ee -api LargeFileSupport=1 -X Viofo139_videos > /tmp/Viofo139.xml
```
```sh
mapillary_tools video_process Viofo139_videos /tmp/Viofo139_samples \
    --geotag_source exiftool \
    --geotag_source_path /tmp/Viofo139.xml \
    --video_sample_distance -1 \
    --video_sample_interval 2

2023-06-12 11:17:31,931 - INFO    - Checking upload status for 469 metadatas
2023-06-12 11:17:31,970 - INFO    - Check the description file for details: /tmp/Viofo139_samples/mapillary_image_description.json
2023-06-12 11:17:31,970 - INFO    -      469 image(s) read in total
2023-06-12 11:17:31,970 - INFO    - 	      441 image(s) are ready to be uploaded
2023-06-12 11:17:31,970 - ERROR   - 	        1 image(s) failed due to MapillaryOutsideGPXTrackError
2023-06-12 11:17:31,970 - WARNING - 	       27 image(s) skipped due to MapillaryDuplicationError
2023-06-12 11:17:31,970 - ERROR   - MapillaryProcessError: Failed to process 1 files. To skip these errors, specify --skip_process_errors
```

Alternatively, you can just process the videos for directly uploading them later:

```
mapillary_tools process Viofo139_videos \
    --geotag_source exiftool \
    --geotag_source_path /tmp/Viofo139.xml


Extracting geotags from ExifTool XML: 0images [00:00, ?images/s]
Extracting GPS tracks from ExifTool XML: 0videos [00:00, ?videos/s]
Extracting GPS tracks from ExifTool XML: 100%|██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 19/19 [00:01<00:00, 13.21videos/s]
2023-06-12 11:22:50,154 - INFO    - Validating 19 metadatas
2023-06-12 11:22:50,262 - INFO    - Checking upload status for 19 metadatas
2023-06-12 11:22:50,272 - INFO    - Check the description file for details: /tmp/image.json
2023-06-12 11:22:50,272 - INFO    -       19 video(s) read in total
2023-06-12 11:22:50,272 - INFO    - 	       19 video(s) are ready to be uploaded
```@stefanbj: Alternative seems to work, if the --skip_process_errors option is used, which does not seem to be the wanted default solution:

Tested with: commit f3c98bdfb46a7df29d8ce8283a4c0fee29d8e185 (HEAD, tag: v0.10.5, origin/main, origin/HEAD)
Test file: [https://drive.google.com/file/d/1xtH88H-Kq2259kyx8kZc-lmSBv1rAfWh/view?usp=drive_link](url) 1.16 GB

```
$ exiftool -r -n -ee -api LargeFileSupport=1 -X K:/Dashcam/2023-07-22 > K:/Dashcam/2023-07-22/Viofo139.xml
    3 directories scanned
 1073 image files read

...
$ python3.exe -m mapillary_tools.commands video_process  K:/Dashcam/2023-07-22/2023_0721_080836_F.MP4 D:/Viofo139_0722     --geotag_source exiftool     --geotag_source_path K:/Dashcam/2023-07-22/Viofo139.xml --video_sample_distance -1 --video_sample_interval 2  --skip_process_errors
2023-07-23 05:13:57,043 - WARNING - Skip sampling video 2023_0721_080836_F.MP4 as it has been sampled in D:\Viofo139_0722\2023_0721_080836_F.MP4. Specify --rerun to resample it
2023-07-23 05:13:57,043 - WARNING - Force the option "filetypes" to be "image" to avoid processing and uploading both the video samples and the videos themselves
Extracting geotags from ExifTool XML:   0%|                                                                                                  | 0/150 [00:00<?, ?images/s]
Extracting GPS tracks from ExifTool XML: 100%|█████████████████████████████████████████████████████████████████████████████████████████| 1/1 [00:02<00:00,  2.97s/videos]
Interpolating 2023_0721_080836_F.MP4: 100%|███████████████████████████████████████████████████████████████████████████████████████| 150/150 [00:01<00:00, 141.09images/s]
Validating metadatas: 100%|█████████████████████████████████████████████████████████████████████████████████████████████████████| 150/150 [00:00<00:00, 254.23metadata/s]
2023-07-23 05:14:06,870 - INFO    - Checking upload status for 150 metadatas
2023-07-23 05:14:06,940 - INFO    - Check the description file for details: D:\Viofo139_0722\mapillary_image_description.json
2023-07-23 05:14:06,941 - INFO    -      150 image(s) read in total
2023-07-23 05:14:06,941 - INFO    -           128 image(s) are ready to be uploaded
2023-07-23 05:14:06,941 - WARNING -            11 image(s) skipped due to MapillaryOutsideGPXTrackError
2023-07-23 05:14:06,942 - WARNING -            11 image(s) skipped due to MapillaryDuplicationError
````

ALso tried the "video_process_and_upload" option:
```
$ python3.exe -m mapillary_tools.commands video_process_and_upload --user_name bruno360   --interpolate_directions   K:/Dashcam/2023-07-22/tmp/2023_0721_080836_F.MP4 D:/Dashcam/tmp/2023_0721_080836_F --geotag_source exiftool     --geotag_source_path K:/Dashcam/2023-07-22/Viofo139.xml --video_sample_distance -1 --video_sample_interval 2 --skip_process_errors
```

Unfortunately there seems to be a mix up with the input-and output paths, so this fails with:
```
2023-07-23 05:52:55,828 - INFO    - Extracting video information: ffprobe -print_format json -hide_banner -show_format -show_streams K:\Dashcam\2023-07-22\tmp\2023_0721_0
80836_F.MP4
Input #0, mov,mp4,m4a,3gp,3g2,mj2, from 'K:\Dashcam\2023-07-22\tmp\2023_0721_080836_F.MP4':
  Metadata:
    major_brand     : mp42
    minor_version   : 0
    compatible_brands: isomavc1mp42
    creation_time   : 2023-07-21T08:13:37.000000Z
  Duration: 00:05:00.00, start: 0.000000, bitrate: 33151 kb/s
  Stream #0:0[0x1](eng): Video: hevc (Main) (hvc1 / 0x31637668), yuvj420p(pc, bt709), 3840x2160, 32769 kb/s, 30 fps, 30 tbr, 60k tbn (default)
    Metadata:
      creation_time   : 2023-07-21T08:13:37.000000Z
      handler_name    : VideoHandler
      vendor_id       : [0][0][0][0]
  Stream #0:1[0x2](eng): Audio: aac (LC) (mp4a / 0x6134706D), 16000 Hz, mono, fltp, 64 kb/s (default)
    Metadata:
      creation_time   : 2023-07-21T08:13:37.000000Z
      handler_name    : SoundHandler
      vendor_id       : [0][0][0][0]
2023-07-23 05:52:55,879 - INFO    - Extracting frames: ffmpeg -hide_banner -nostdin -i K:\Dashcam\2023-07-22\tmp\2023_0721_080836_F.MP4 -vf fps=1/2.0 -qscale:v 2 D:\Dashc
am\tmp\2023_0721_080836_F\.mly_ffmpeg_2023_0721_080836_F.MP4_17404_1690087975\2023_0721_080836_F_NA_%06d.jpg
Input #0, mov,mp4,m4a,3gp,3g2,mj2, from 'K:\Dashcam\2023-07-22\tmp\2023_0721_080836_F.MP4':
  Metadata:
    major_brand     : mp42
    minor_version   : 0
    compatible_brands: isomavc1mp42
    creation_time   : 2023-07-21T08:13:37.000000Z
  Duration: 00:05:00.00, start: 0.000000, bitrate: 33151 kb/s
  Stream #0:0[0x1](eng): Video: hevc (Main) (hvc1 / 0x31637668), yuvj420p(pc, bt709), 3840x2160, 32769 kb/s, 30 fps, 30 tbr, 60k tbn (default)
    Metadata:
      creation_time   : 2023-07-21T08:13:37.000000Z
      handler_name    : VideoHandler
      vendor_id       : [0][0][0][0]
  Stream #0:1[0x2](eng): Audio: aac (LC) (mp4a / 0x6134706D), 16000 Hz, mono, fltp, 64 kb/s (default)
    Metadata:
      creation_time   : 2023-07-21T08:13:37.000000Z
      handler_name    : SoundHandler
      vendor_id       : [0][0][0][0]
Stream mapping:
  Stream #0:0 -> #0:0 (hevc (native) -> mjpeg (native))
[in#0/mov,mp4,m4a,3gp,3g2,mj2 @ 000001d3986eb240] New audio stream with index 1 at pos:1384596 and DTS:0s
Output #0, image2, to 'D:\Dashcam\tmp\2023_0721_080836_F\.mly_ffmpeg_2023_0721_080836_F.MP4_17404_1690087975\2023_0721_080836_F_NA_%06d.jpg':
  Metadata:
    major_brand     : mp42
    minor_version   : 0
    compatible_brands: isomavc1mp42
    encoder         : Lavf60.5.100
  Stream #0:0(eng): Video: mjpeg, yuvj420p(pc, bt709, progressive), 3840x2160, q=2-31, 200 kb/s, 0.50 fps, 0.50 tbn (default)
    Metadata:
      creation_time   : 2023-07-21T08:13:37.000000Z
      handler_name    : VideoHandler
      vendor_id       : [0][0][0][0]
      encoder         : Lavc60.14.101 mjpeg
    Side data:
      cpb: bitrate max/min/avg: 0/0/200000 buffer size: 0 vbv_delay: N/A
[out#0/image2 @ 000001d398726fc0] video:209241kB audio:0kB subtitle:0kB other streams:0kB global headers:0kB muxing overhead: unknown
frame=  150 fps=1.2 q=2.0 Lsize=N/A time=00:04:58.00 bitrate=N/A speed=2.39x
2023-07-23 05:55:01,472 - WARNING - Force the option "filetypes" to be "image" to avoid processing and uploading both the video samples and the videos themselves
Extracting geotags from ExifTool XML:   0%|                                                                                                  | 0/150 [00:00<?, ?images/s]
Extracting GPS tracks from ExifTool XML:   0%|                                                                                                 | 0/1 [00:00<?, ?videos/s]
Failed to geotag video K:\Dashcam\2023-07-22\2023_0721_080836_F.MP4: [Errno 2] No such file or directory: 'K:\\Dashcam\\2023-07-22\\2023_0721_080836_F.MP4'
Extracting GPS tracks from ExifTool XML: 100%|█████████████████████████████████████████████████████████████████████████████████████████| 1/1 [00:00<00:00,  3.62videos/s]
Validating metadatas: 100%|██████████████████████████████████████████████████████████████████████████████████████████████████████████████| 150/150 [00:00<?, ?metadata/s]
2023-07-23 05:55:06,902 - INFO    - Checking upload status for 150 metadatas
2023-07-23 05:55:06,902 - INFO    -      150 image(s) read in total
2023-07-23 05:55:06,903 - WARNING -           150 image(s) skipped due to FileNotFoundError
2023-07-23 05:55:06,970 - INFO    - Nothing uploaded. Bye.

```



The "process" alternative seems to want to upload the raw .MP4 file (which will likely be slower then to process and uploaded the selected images); and then I just don't see the point of using mapillary_tools for most users -- would likely be better to simply offer upload of .MP4 files in the web-front-end...

Also interrupting the process doesn't work correctly on windows/cygwin -- it gets tuck in some kind of limbo state:
```
$ python3.exe -m mapillary_tools.commands process  K:/Dashcam/2023-07-22/tmp/2023_0721_080836_F.MP4     --geotag_source exiftool     --geotag_source_path K:/Dashcam/2023-07-22/tmp/Viofo139.xml
Extracting geotags from ExifTool XML: 0images [00:00, ?images/s]
Extracting GPS tracks from ExifTool XML: 0videos [00:00, ?videos/s]
Extracting GPS tracks from ExifTool XML:   0%|                                                                                                 | 0/1 [00:00<?, ?videos/s]P
rocess SpawnPoolWorker-24:
Process SpawnPoolWorker-23:
Process SpawnPoolWorker-22:
Process SpawnPoolWorker-21:
Process SpawnPoolWorker-20:
Process SpawnPoolWorker-19:
Process SpawnPoolWorker-18:
Traceback (most recent call last):
  File "C:\Program Files\WindowsApps\PythonSoftwareFoundation.Python.3.10_3.10.3056.0_x64__qbz5n2kfra8p0\lib\multiprocessing\process.py", line 314, in _bootstrap
    self.run()
Traceback (most recent call last):
Traceback (most recent call last):
Traceback (most recent call last):
Traceback (most recent call last):
Traceback (most recent call last):
Traceback (most recent call last):
  File "C:\Program Files\WindowsApps\PythonSoftwareFoundation.Python.3.10_3.10.3056.0_x64__qbz5n2kfra8p0\lib\multiprocessing\process.py", line 108, in run
    self._target(*self._args, **self._kwargs)
  File "C:\Program Files\WindowsApps\PythonSoftwareFoundation.Python.3.10_3.10.3056.0_x64__qbz5n2kfra8p0\lib\multiprocessing\process.py", line 314, in _bootstrap
    self.run()
  File "C:\Program Files\WindowsApps\PythonSoftwareFoundation.Python.3.10_3.10.3056.0_x64__qbz5n2kfra8p0\lib\multiprocessing\process.py", line 314, in _bootstrap
    self.run()
  File "C:\Program Files\WindowsApps\PythonSoftwareFoundation.Python.3.10_3.10.3056.0_x64__qbz5n2kfra8p0\lib\multiprocessing\process.py", line 314, in _bootstrap
    self.run()
  File "C:\Program Files\WindowsApps\PythonSoftwareFoundation.Python.3.10_3.10.3056.0_x64__qbz5n2kfra8p0\lib\multiprocessing\process.py", line 314, in _bootstrap
    self.run()
  File "C:\Program Files\WindowsApps\PythonSoftwareFoundation.Python.3.10_3.10.3056.0_x64__qbz5n2kfra8p0\lib\multiprocessing\process.py", line 314, in _bootstrap
    self.run()
  File "C:\Program Files\WindowsApps\PythonSoftwareFoundation.Python.3.10_3.10.3056.0_x64__qbz5n2kfra8p0\lib\multiprocessing\process.py", line 314, in _bootstrap
    self.run()
  File "C:\Program Files\WindowsApps\PythonSoftwareFoundation.Python.3.10_3.10.3056.0_x64__qbz5n2kfra8p0\lib\multiprocessing\pool.py", line 114, in worker
    task = get()
  File "C:\Program Files\WindowsApps\PythonSoftwareFoundation.Python.3.10_3.10.3056.0_x64__qbz5n2kfra8p0\lib\multiprocessing\process.py", line 108, in run
    self._target(*self._args, **self._kwargs)
  File "C:\Program Files\WindowsApps\PythonSoftwareFoundation.Python.3.10_3.10.3056.0_x64__qbz5n2kfra8p0\lib\multiprocessing\process.py", line 108, in run
    self._target(*self._args, **self._kwargs)
  File "C:\Program Files\WindowsApps\PythonSoftwareFoundation.Python.3.10_3.10.3056.0_x64__qbz5n2kfra8p0\lib\multiprocessing\process.py", line 108, in run
    self._target(*self._args, **self._kwargs)
  File "C:\Program Files\WindowsApps\PythonSoftwareFoundation.Python.3.10_3.10.3056.0_x64__qbz5n2kfra8p0\lib\multiprocessing\process.py", line 108, in run
    self._target(*self._args, **self._kwargs)
  File "C:\Program Files\WindowsApps\PythonSoftwareFoundation.Python.3.10_3.10.3056.0_x64__qbz5n2kfra8p0\lib\multiprocessing\process.py", line 108, in run
    self._target(*self._args, **self._kwargs)
  File "C:\Program Files\WindowsApps\PythonSoftwareFoundation.Python.3.10_3.10.3056.0_x64__qbz5n2kfra8p0\lib\multiprocessing\process.py", line 108, in run
    self._target(*self._args, **self._kwargs)
  File "C:\Program Files\WindowsApps\PythonSoftwareFoundation.Python.3.10_3.10.3056.0_x64__qbz5n2kfra8p0\lib\multiprocessing\queues.py", line 364, in get
    with self._rlock:
  File "C:\Program Files\WindowsApps\PythonSoftwareFoundation.Python.3.10_3.10.3056.0_x64__qbz5n2kfra8p0\lib\multiprocessing\pool.py", line 114, in worker
    task = get()
  File "C:\Program Files\WindowsApps\PythonSoftwareFoundation.Python.3.10_3.10.3056.0_x64__qbz5n2kfra8p0\lib\multiprocessing\pool.py", line 114, in worker
    task = get()
  File "C:\Program Files\WindowsApps\PythonSoftwareFoundation.Python.3.10_3.10.3056.0_x64__qbz5n2kfra8p0\lib\multiprocessing\pool.py", line 114, in worker
    task = get()
  File "C:\Program Files\WindowsApps\PythonSoftwareFoundation.Python.3.10_3.10.3056.0_x64__qbz5n2kfra8p0\lib\multiprocessing\pool.py", line 114, in worker
    task = get()
  File "C:\Program Files\WindowsApps\PythonSoftwareFoundation.Python.3.10_3.10.3056.0_x64__qbz5n2kfra8p0\lib\multiprocessing\pool.py", line 114, in worker
    task = get()
  File "C:\Program Files\WindowsApps\PythonSoftwareFoundation.Python.3.10_3.10.3056.0_x64__qbz5n2kfra8p0\lib\multiprocessing\pool.py", line 114, in worker
    task = get()
  File "C:\Program Files\WindowsApps\PythonSoftwareFoundation.Python.3.10_3.10.3056.0_x64__qbz5n2kfra8p0\lib\multiprocessing\synchronize.py", line 95, in __enter__
    return self._semlock.__enter__()
  File "C:\Program Files\WindowsApps\PythonSoftwareFoundation.Python.3.10_3.10.3056.0_x64__qbz5n2kfra8p0\lib\multiprocessing\queues.py", line 364, in get
    with self._rlock:
  File "C:\Program Files\WindowsApps\PythonSoftwareFoundation.Python.3.10_3.10.3056.0_x64__qbz5n2kfra8p0\lib\multiprocessing\queues.py", line 364, in get
    with self._rlock:
  File "C:\Program Files\WindowsApps\PythonSoftwareFoundation.Python.3.10_3.10.3056.0_x64__qbz5n2kfra8p0\lib\multiprocessing\queues.py", line 364, in get
    with self._rlock:
  File "C:\Program Files\WindowsApps\PythonSoftwareFoundation.Python.3.10_3.10.3056.0_x64__qbz5n2kfra8p0\lib\multiprocessing\queues.py", line 364, in get
    with self._rlock:
  File "C:\Program Files\WindowsApps\PythonSoftwareFoundation.Python.3.10_3.10.3056.0_x64__qbz5n2kfra8p0\lib\multiprocessing\queues.py", line 364, in get
    with self._rlock:
  File "C:\Program Files\WindowsApps\PythonSoftwareFoundation.Python.3.10_3.10.3056.0_x64__qbz5n2kfra8p0\lib\multiprocessing\queues.py", line 365, in get
    res = self._reader.recv_bytes()
KeyboardInterrupt
  File "C:\Program Files\WindowsApps\PythonSoftwareFoundation.Python.3.10_3.10.3056.0_x64__qbz5n2kfra8p0\lib\multiprocessing\synchronize.py", line 95, in __enter__
    return self._semlock.__enter__()
  File "C:\Program Files\WindowsApps\PythonSoftwareFoundation.Python.3.10_3.10.3056.0_x64__qbz5n2kfra8p0\lib\multiprocessing\synchronize.py", line 95, in __enter__
    return self._semlock.__enter__()
  File "C:\Program Files\WindowsApps\PythonSoftwareFoundation.Python.3.10_3.10.3056.0_x64__qbz5n2kfra8p0\lib\multiprocessing\synchronize.py", line 95, in __enter__
    return self._semlock.__enter__()
  File "C:\Program Files\WindowsApps\PythonSoftwareFoundation.Python.3.10_3.10.3056.0_x64__qbz5n2kfra8p0\lib\multiprocessing\synchronize.py", line 95, in __enter__
    return self._semlock.__enter__()
  File "C:\Program Files\WindowsApps\PythonSoftwareFoundation.Python.3.10_3.10.3056.0_x64__qbz5n2kfra8p0\lib\multiprocessing\synchronize.py", line 95, in __enter__
    return self._semlock.__enter__()
  File "C:\Program Files\WindowsApps\PythonSoftwareFoundation.Python.3.10_3.10.3056.0_x64__qbz5n2kfra8p0\lib\multiprocessing\connection.py", line 216, in recv_bytes
    buf = self._recv_bytes(maxlength)
KeyboardInterrupt
KeyboardInterrupt
KeyboardInterrupt
KeyboardInterrupt
KeyboardInterrupt
  File "C:\Program Files\WindowsApps\PythonSoftwareFoundation.Python.3.10_3.10.3056.0_x64__qbz5n2kfra8p0\lib\multiprocessing\connection.py", line 305, in _recv_bytes
    waitres = _winapi.WaitForMultipleObjects(
KeyboardInterrupt
Process SpawnPoolWorker-17:
Traceback (most recent call last):
  File "C:\Program Files\WindowsApps\PythonSoftwareFoundation.Python.3.10_3.10.3056.0_x64__qbz5n2kfra8p0\lib\multiprocessing\process.py", line 314, in _bootstrap
    self.run()
  File "C:\Program Files\WindowsApps\PythonSoftwareFoundation.Python.3.10_3.10.3056.0_x64__qbz5n2kfra8p0\lib\multiprocessing\process.py", line 108, in run
    self._target(*self._args, **self._kwargs)
  File "C:\Program Files\WindowsApps\PythonSoftwareFoundation.Python.3.10_3.10.3056.0_x64__qbz5n2kfra8p0\lib\multiprocessing\pool.py", line 125, in worker
    result = (True, func(*args, **kwds))
  File "C:\Users\stefanb\Desktop\OUTDIR\mt\mapillary_tools\mapillary_tools\geotag\geotag_videos_from_exiftool_video.py", line 76, in geotag_video
    video_metadata.update_md5sum()
  File "C:\Users\stefanb\Desktop\OUTDIR\mt\mapillary_tools\mapillary_tools\types.py", line 93, in update_md5sum
    self.md5sum = utils.md5sum_fp(fp).hexdigest()
  File "C:\Users\stefanb\Desktop\OUTDIR\mt\mapillary_tools\mapillary_tools\utils.py", line 15, in md5sum_fp
    buf = fp.read(1024 * 1024 * 32)
KeyboardInterrupt

````
@stefanbj: Uploading 'A138 videos using the new Mapillary Uploader seems to work fine, so seems the feature as such is working as is should. (Thanks, much appreciated.)

Unfortunately I'm having less luck with getting the New video geotagging features interface working, to verify this from the command line, and the trackbacks are not exactly helpful w.r.t. understating what I'm (most likely) doing incorrectly...

1. gpx source, implicit pattern:

```
PS I:\Dashcam\2023-07-28> exiftool -ver
12.55
PS I:\Dashcam\2023-07-28> python.exe -m mapillary_tools.commands --version
mapillary_tools version 0.11.1
PS I:\Dashcam\2023-07-28> ls  2023_0728_111630_F.*


    Directory: I:\Dashcam\2023-07-28


Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
-a----        2024-01-08     01:23          30791 2023_0728_111630_F.gpx
-a----        2023-07-28     11:21     1243169655 2023_0728_111630_F.MP4

PS I:\Dashcam\2023-07-28> ls D:/Bilder/tmp/2023_0728_111630_F


    Directory: D:\Bilder\tmp\2023_0728_111630_F


Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
-a----        2024-01-08     01:03          30791 2023_0728_111630_F.gpx


PS I:\Dashcam\2023-07-28> python.exe -m mapillary_tools.commands --verbose video_process_and_upload --user_name bruno360 --video_geotag_source gpx --interpolate_directions 2023_0728_111630_F.MP4 D:/Bilder/tmp/2023_0728_111630_F
2024-01-08 08:17:40,319 - DEBUG   - mapillary_tools version 0.11.1
2024-01-08 08:17:40,319 - DEBUG   - CLI param: verbose: True
2024-01-08 08:17:40,320 - DEBUG   - CLI param: video_import_path: 2023_0728_111630_F.MP4
2024-01-08 08:17:40,321 - DEBUG   - CLI param: import_path: D:\Bilder\tmp\2023_0728_111630_F
2024-01-08 08:17:40,322 - DEBUG   - CLI param: video_sample_distance: 3.0
2024-01-08 08:17:40,323 - DEBUG   - CLI param: video_sample_interval: -1.0
2024-01-08 08:17:40,323 - DEBUG   - CLI param: video_duration_ratio: 1.0
2024-01-08 08:17:40,324 - DEBUG   - CLI param: skip_subfolders: False
2024-01-08 08:17:40,324 - DEBUG   - CLI param: rerun: False
2024-01-08 08:17:40,324 - DEBUG   - CLI param: skip_sample_errors: False
2024-01-08 08:17:40,325 - DEBUG   - CLI param: skip_process_errors: False
2024-01-08 08:17:40,325 - DEBUG   - CLI param: overwrite_all_EXIF_tags: False
2024-01-08 08:17:40,326 - DEBUG   - CLI param: overwrite_EXIF_time_tag: False
2024-01-08 08:17:40,326 - DEBUG   - CLI param: overwrite_EXIF_gps_tag: False
2024-01-08 08:17:40,326 - DEBUG   - CLI param: overwrite_EXIF_direction_tag: False
2024-01-08 08:17:40,327 - DEBUG   - CLI param: overwrite_EXIF_orientation_tag: False
2024-01-08 08:17:40,327 - DEBUG   - CLI param: add_file_name: False
2024-01-08 08:17:40,327 - DEBUG   - CLI param: add_import_date: False
2024-01-08 08:17:40,329 - DEBUG   - CLI param: geotag_source: exif
2024-01-08 08:17:40,330 - DEBUG   - CLI param: video_geotag_source: gpx
2024-01-08 08:17:40,330 - DEBUG   - CLI param: interpolation_use_gpx_start_time: False
2024-01-08 08:17:40,330 - DEBUG   - CLI param: interpolation_offset_time: 0.0
2024-01-08 08:17:40,331 - DEBUG   - CLI param: offset_angle: 0.0
2024-01-08 08:17:40,333 - DEBUG   - CLI param: offset_time: 0.0
2024-01-08 08:17:40,333 - DEBUG   - CLI param: cutoff_distance: 600.0
2024-01-08 08:17:40,333 - DEBUG   - CLI param: cutoff_time: 60.0
2024-01-08 08:17:40,333 - DEBUG   - CLI param: interpolate_directions: True
2024-01-08 08:17:40,334 - DEBUG   - CLI param: duplicate_distance: 0.1
2024-01-08 08:17:40,334 - DEBUG   - CLI param: duplicate_angle: 5.0
2024-01-08 08:17:40,335 - DEBUG   - CLI param: filetypes: blackvue, camm, gopro, image, video
2024-01-08 08:17:40,335 - DEBUG   - CLI param: user_name: bruno360
2024-01-08 08:17:40,335 - DEBUG   - CLI param: dry_run: False
2024-01-08 08:17:40,338 - INFO    - Extracting video information: ffprobe -print_format json -hide_banner -show_format -show_streams 2023_0728_111630_F.MP4
Input #0, mov,mp4,m4a,3gp,3g2,mj2, from '2023_0728_111630_F.MP4':
  Metadata:
    major_brand     : mp42
    minor_version   : 0
    compatible_brands: isomavc1mp42
    creation_time   : 2023-07-28T11:21:30.000000Z
  Duration: 00:05:00.00, start: 0.000000, bitrate: 33151 kb/s
  Stream #0:0[0x1](eng): Video: hevc (Main) (hvc1 / 0x31637668), yuvj420p(pc, bt709), 3840x2160, 32768 kb/s, 30 fps, 30 tbr, 60k tbn (default)
    Metadata:
      creation_time   : 2023-07-28T11:21:30.000000Z
      handler_name    : VideoHandler
      vendor_id       : [0][0][0][0]
  Stream #0:1[0x2](eng): Audio: aac (LC) (mp4a / 0x6134706D), 16000 Hz, mono, fltp, 64 kb/s (default)
    Metadata:
      creation_time   : 2023-07-28T11:21:30.000000Z
      handler_name    : SoundHandler
      vendor_id       : [0][0][0][0]
2024-01-08 08:17:40,392 - DEBUG   - Extracted video duration: 300.000000
2024-01-08 08:17:40,394 - DEBUG   - Extracted video creation time: 2023-07-28T11:21:30.000000Z
2024-01-08 08:17:40,399 - INFO    - Extracting video metdata
2024-01-08 08:17:40,401 - WARNING - Failed to geotag video 2023_0728_111630_F.MP4: No GPS data found from the video
Traceback (most recent call last):
  File "C:\Program Files\Python310\lib\site-packages\mapillary_tools\geotag\geotag_videos_from_video.py", line 155, in geotag_video
    raise exceptions.MapillaryVideoError("No GPS data found from the video")
mapillary_tools.exceptions.MapillaryVideoError: No GPS data found from the video
2024-01-08 08:17:40,401 - WARNING - No GPS data found from the video
2024-01-08 08:17:40,402 - WARNING - Force the option "filetypes" to be "image" to avoid processing and uploading both the video samples and the videos themselves
Traceback (most recent call last):
  File "C:\Program Files\Python310\lib\runpy.py", line 196, in _run_module_as_main
    return _run_code(code, main_globals, None,
  File "C:\Program Files\Python310\lib\runpy.py", line 86, in _run_code
    exec(code, run_globals)
  File "C:\Program Files\Python310\lib\site-packages\mapillary_tools\commands\__main__.py", line 177, in <module>
    main()
  File "C:\Program Files\Python310\lib\site-packages\mapillary_tools\commands\__main__.py", line 168, in main
    args.func(argvars)
  File "C:\Program Files\Python310\lib\site-packages\mapillary_tools\commands\video_process_and_upload.py", line 18, in run
    VideoProcessCommand().run(args)
  File "C:\Program Files\Python310\lib\site-packages\mapillary_tools\commands\video_process.py", line 31, in run
    ProcessCommand().run(args)
  File "C:\Program Files\Python310\lib\site-packages\mapillary_tools\commands\process.py", line 270, in run
    metadatas = process_geotag_properties(
  File "C:\Program Files\Python310\lib\site-packages\mapillary_tools\process_geotag_properties.py", line 234, in process_geotag_properties
    metadatas.extend(_process_videos_beta(vars_args))
  File "C:\Program Files\Python310\lib\site-packages\mapillary_tools\process_geotag_properties.py", line 295, in _process_videos_beta
    return extractor.process()
  File "C:\Program Files\Python310\lib\site-packages\mapillary_tools\video_data_extraction\extract_video_data.py", line 35, in process
    self._check_paths(paths)
  File "C:\Program Files\Python310\lib\site-packages\mapillary_tools\video_data_extraction\extract_video_data.py", line 134, in _check_paths
    for path in import_paths:
TypeError: 'WindowsPath' object is not iterable
PS I:\Dashcam\2023-07-28>

```

2. gpx source, explicit pattern:
```
PS I:\Dashcam\2023-07-28> python.exe -m mapillary_tools.commands --verbose video_process_and_upload --user_name bruno360 --video_geotag_source '{"source": "gpx", "pattern": "%g.gpx"}' --interpolate_directions 2023_0728_111630_F.MP4 D:/Bilder/tmp/2023_0728_111630_F
2024-01-08 08:16:36,927 - DEBUG   - mapillary_tools version 0.11.1
2024-01-08 08:16:36,927 - DEBUG   - CLI param: verbose: True
2024-01-08 08:16:36,929 - DEBUG   - CLI param: video_import_path: 2023_0728_111630_F.MP4
2024-01-08 08:16:36,930 - DEBUG   - CLI param: import_path: D:\Bilder\tmp\2023_0728_111630_F
2024-01-08 08:16:36,930 - DEBUG   - CLI param: video_sample_distance: 3.0
2024-01-08 08:16:36,931 - DEBUG   - CLI param: video_sample_interval: -1.0
2024-01-08 08:16:36,931 - DEBUG   - CLI param: video_duration_ratio: 1.0
2024-01-08 08:16:36,931 - DEBUG   - CLI param: skip_subfolders: False
2024-01-08 08:16:36,932 - DEBUG   - CLI param: rerun: False
2024-01-08 08:16:36,932 - DEBUG   - CLI param: skip_sample_errors: False
2024-01-08 08:16:36,932 - DEBUG   - CLI param: skip_process_errors: False
2024-01-08 08:16:36,933 - DEBUG   - CLI param: overwrite_all_EXIF_tags: False
2024-01-08 08:16:36,933 - DEBUG   - CLI param: overwrite_EXIF_time_tag: False
2024-01-08 08:16:36,933 - DEBUG   - CLI param: overwrite_EXIF_gps_tag: False
2024-01-08 08:16:36,934 - DEBUG   - CLI param: overwrite_EXIF_direction_tag: False
2024-01-08 08:16:36,934 - DEBUG   - CLI param: overwrite_EXIF_orientation_tag: False
2024-01-08 08:16:36,934 - DEBUG   - CLI param: add_file_name: False
2024-01-08 08:16:36,935 - DEBUG   - CLI param: add_import_date: False
2024-01-08 08:16:36,935 - DEBUG   - CLI param: geotag_source: exif
2024-01-08 08:16:36,935 - DEBUG   - CLI param: video_geotag_source: {source: gpx, pattern: %g.gpx}
2024-01-08 08:16:36,936 - DEBUG   - CLI param: interpolation_use_gpx_start_time: False
2024-01-08 08:16:36,936 - DEBUG   - CLI param: interpolation_offset_time: 0.0
2024-01-08 08:16:36,937 - DEBUG   - CLI param: offset_angle: 0.0
2024-01-08 08:16:36,939 - DEBUG   - CLI param: offset_time: 0.0
2024-01-08 08:16:36,939 - DEBUG   - CLI param: cutoff_distance: 600.0
2024-01-08 08:16:36,939 - DEBUG   - CLI param: cutoff_time: 60.0
2024-01-08 08:16:36,940 - DEBUG   - CLI param: interpolate_directions: True
2024-01-08 08:16:36,940 - DEBUG   - CLI param: duplicate_distance: 0.1
2024-01-08 08:16:36,941 - DEBUG   - CLI param: duplicate_angle: 5.0
2024-01-08 08:16:36,941 - DEBUG   - CLI param: filetypes: blackvue, camm, gopro, image, video
2024-01-08 08:16:36,941 - DEBUG   - CLI param: user_name: bruno360
2024-01-08 08:16:36,942 - DEBUG   - CLI param: dry_run: False
2024-01-08 08:16:36,944 - INFO    - Extracting video information: ffprobe -print_format json -hide_banner -show_format -show_streams 2023_0728_111630_F.MP4
Input #0, mov,mp4,m4a,3gp,3g2,mj2, from '2023_0728_111630_F.MP4':
  Metadata:
    major_brand     : mp42
    minor_version   : 0
    compatible_brands: isomavc1mp42
    creation_time   : 2023-07-28T11:21:30.000000Z
  Duration: 00:05:00.00, start: 0.000000, bitrate: 33151 kb/s
  Stream #0:0[0x1](eng): Video: hevc (Main) (hvc1 / 0x31637668), yuvj420p(pc, bt709), 3840x2160, 32768 kb/s, 30 fps, 30 tbr, 60k tbn (default)
    Metadata:
      creation_time   : 2023-07-28T11:21:30.000000Z
      handler_name    : VideoHandler
      vendor_id       : [0][0][0][0]
  Stream #0:1[0x2](eng): Audio: aac (LC) (mp4a / 0x6134706D), 16000 Hz, mono, fltp, 64 kb/s (default)
    Metadata:
      creation_time   : 2023-07-28T11:21:30.000000Z
      handler_name    : SoundHandler
      vendor_id       : [0][0][0][0]
2024-01-08 08:16:36,992 - DEBUG   - Extracted video duration: 300.000000
2024-01-08 08:16:36,992 - DEBUG   - Extracted video creation time: 2023-07-28T11:21:30.000000Z
2024-01-08 08:16:36,995 - INFO    - Extracting video metdata
2024-01-08 08:16:36,997 - WARNING - Failed to geotag video 2023_0728_111630_F.MP4: No GPS data found from the video
Traceback (most recent call last):
  File "C:\Program Files\Python310\lib\site-packages\mapillary_tools\geotag\geotag_videos_from_video.py", line 155, in geotag_video
    raise exceptions.MapillaryVideoError("No GPS data found from the video")
mapillary_tools.exceptions.MapillaryVideoError: No GPS data found from the video
2024-01-08 08:16:36,998 - WARNING - No GPS data found from the video
2024-01-08 08:16:36,998 - WARNING - Force the option "filetypes" to be "image" to avoid processing and uploading both the video samples and the videos themselves
2024-01-08 08:16:37,001 - ERROR   - MapillaryBadParameterError: ('Unknown beta source %s or invalid JSON', '{source: gpx, pattern: %g.gpx}')
Traceback (most recent call last):
  File "C:\Program Files\Python310\lib\site-packages\mapillary_tools\process_geotag_properties.py", line 271, in _process_videos_beta
    parsed_opts = json.loads(source)
  File "C:\Program Files\Python310\lib\json\__init__.py", line 346, in loads
    return _default_decoder.decode(s)
  File "C:\Program Files\Python310\lib\json\decoder.py", line 337, in decode
    obj, end = self.raw_decode(s, idx=_w(s, 0).end())
  File "C:\Program Files\Python310\lib\json\decoder.py", line 353, in raw_decode
    obj, end = self.scan_once(s, idx)
json.decoder.JSONDecodeError: Expecting property name enclosed in double quotes: line 1 column 2 (char 1)

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "C:\Program Files\Python310\lib\site-packages\mapillary_tools\commands\__main__.py", line 168, in main
    args.func(argvars)
  File "C:\Program Files\Python310\lib\site-packages\mapillary_tools\commands\video_process_and_upload.py", line 18, in run
    VideoProcessCommand().run(args)
  File "C:\Program Files\Python310\lib\site-packages\mapillary_tools\commands\video_process.py", line 31, in run
    ProcessCommand().run(args)
  File "C:\Program Files\Python310\lib\site-packages\mapillary_tools\commands\process.py", line 270, in run
    metadatas = process_geotag_properties(
  File "C:\Program Files\Python310\lib\site-packages\mapillary_tools\process_geotag_properties.py", line 234, in process_geotag_properties
    metadatas.extend(_process_videos_beta(vars_args))
  File "C:\Program Files\Python310\lib\site-packages\mapillary_tools\process_geotag_properties.py", line 274, in _process_videos_beta
    raise exceptions.MapillaryBadParameterError(
mapillary_tools.exceptions.MapillaryBadParameterError: ('Unknown beta source %s or invalid JSON', '{source: gpx, pattern: %g.gpx}')
PS I:\Dashcam\2023-07-28>

```

3. exiftool_runtime, implicit pattern:

```
PS I:\Dashcam\2023-07-28>  python.exe -m mapillary_tools.commands --verbose video_process_and_upload --user_name bruno360 --video_geotag_source exiftool_runtime --interpolate_directions 2023_0728_111630_F.MP4 D:/Bilder/tmp/2023_0728_111630_F
2024-01-08 08:15:43,471 - DEBUG   - mapillary_tools version 0.11.1
2024-01-08 08:15:43,472 - DEBUG   - CLI param: verbose: True
2024-01-08 08:15:43,472 - DEBUG   - CLI param: video_import_path: 2023_0728_111630_F.MP4
2024-01-08 08:15:43,472 - DEBUG   - CLI param: import_path: D:\Bilder\tmp\2023_0728_111630_F
2024-01-08 08:15:43,472 - DEBUG   - CLI param: video_sample_distance: 3.0
2024-01-08 08:15:43,472 - DEBUG   - CLI param: video_sample_interval: -1.0
2024-01-08 08:15:43,472 - DEBUG   - CLI param: video_duration_ratio: 1.0
2024-01-08 08:15:43,472 - DEBUG   - CLI param: skip_subfolders: False
2024-01-08 08:15:43,472 - DEBUG   - CLI param: rerun: False
2024-01-08 08:15:43,472 - DEBUG   - CLI param: skip_sample_errors: False
2024-01-08 08:15:43,473 - DEBUG   - CLI param: skip_process_errors: False
2024-01-08 08:15:43,473 - DEBUG   - CLI param: overwrite_all_EXIF_tags: False
2024-01-08 08:15:43,473 - DEBUG   - CLI param: overwrite_EXIF_time_tag: False
2024-01-08 08:15:43,473 - DEBUG   - CLI param: overwrite_EXIF_gps_tag: False
2024-01-08 08:15:43,473 - DEBUG   - CLI param: overwrite_EXIF_direction_tag: False
2024-01-08 08:15:43,473 - DEBUG   - CLI param: overwrite_EXIF_orientation_tag: False
2024-01-08 08:15:43,473 - DEBUG   - CLI param: add_file_name: False
2024-01-08 08:15:43,473 - DEBUG   - CLI param: add_import_date: False
2024-01-08 08:15:43,473 - DEBUG   - CLI param: geotag_source: exif
2024-01-08 08:15:43,474 - DEBUG   - CLI param: video_geotag_source: exiftool_runtime
2024-01-08 08:15:43,474 - DEBUG   - CLI param: interpolation_use_gpx_start_time: False
2024-01-08 08:15:43,475 - DEBUG   - CLI param: interpolation_offset_time: 0.0
2024-01-08 08:15:43,475 - DEBUG   - CLI param: offset_angle: 0.0
2024-01-08 08:15:43,475 - DEBUG   - CLI param: offset_time: 0.0
2024-01-08 08:15:43,476 - DEBUG   - CLI param: cutoff_distance: 600.0
2024-01-08 08:15:43,476 - DEBUG   - CLI param: cutoff_time: 60.0
2024-01-08 08:15:43,476 - DEBUG   - CLI param: interpolate_directions: True
2024-01-08 08:15:43,477 - DEBUG   - CLI param: duplicate_distance: 0.1
2024-01-08 08:15:43,477 - DEBUG   - CLI param: duplicate_angle: 5.0
2024-01-08 08:15:43,478 - DEBUG   - CLI param: filetypes: blackvue, camm, gopro, image, video
2024-01-08 08:15:43,480 - DEBUG   - CLI param: user_name: bruno360
2024-01-08 08:15:43,480 - DEBUG   - CLI param: dry_run: False
2024-01-08 08:15:43,483 - INFO    - Extracting video information: ffprobe -print_format json -hide_banner -show_format -show_streams 2023_0728_111630_F.MP4
Input #0, mov,mp4,m4a,3gp,3g2,mj2, from '2023_0728_111630_F.MP4':
  Metadata:
    major_brand     : mp42
    minor_version   : 0
    compatible_brands: isomavc1mp42
    creation_time   : 2023-07-28T11:21:30.000000Z
  Duration: 00:05:00.00, start: 0.000000, bitrate: 33151 kb/s
  Stream #0:0[0x1](eng): Video: hevc (Main) (hvc1 / 0x31637668), yuvj420p(pc, bt709), 3840x2160, 32768 kb/s, 30 fps, 30 tbr, 60k tbn (default)
    Metadata:
      creation_time   : 2023-07-28T11:21:30.000000Z
      handler_name    : VideoHandler
      vendor_id       : [0][0][0][0]
  Stream #0:1[0x2](eng): Audio: aac (LC) (mp4a / 0x6134706D), 16000 Hz, mono, fltp, 64 kb/s (default)
    Metadata:
      creation_time   : 2023-07-28T11:21:30.000000Z
      handler_name    : SoundHandler
      vendor_id       : [0][0][0][0]
2024-01-08 08:15:43,531 - DEBUG   - Extracted video duration: 300.000000
2024-01-08 08:15:43,532 - DEBUG   - Extracted video creation time: 2023-07-28T11:21:30.000000Z
2024-01-08 08:15:43,535 - INFO    - Extracting video metdata
2024-01-08 08:15:43,537 - WARNING - Failed to geotag video 2023_0728_111630_F.MP4: No GPS data found from the video
Traceback (most recent call last):
  File "C:\Program Files\Python310\lib\site-packages\mapillary_tools\geotag\geotag_videos_from_video.py", line 155, in geotag_video
    raise exceptions.MapillaryVideoError("No GPS data found from the video")
mapillary_tools.exceptions.MapillaryVideoError: No GPS data found from the video
2024-01-08 08:15:43,538 - WARNING - No GPS data found from the video
2024-01-08 08:15:43,538 - WARNING - Force the option "filetypes" to be "image" to avoid processing and uploading both the video samples and the videos themselves
Traceback (most recent call last):
  File "C:\Program Files\Python310\lib\runpy.py", line 196, in _run_module_as_main
    return _run_code(code, main_globals, None,
  File "C:\Program Files\Python310\lib\runpy.py", line 86, in _run_code
    exec(code, run_globals)
  File "C:\Program Files\Python310\lib\site-packages\mapillary_tools\commands\__main__.py", line 177, in <module>
    main()
  File "C:\Program Files\Python310\lib\site-packages\mapillary_tools\commands\__main__.py", line 168, in main
    args.func(argvars)
  File "C:\Program Files\Python310\lib\site-packages\mapillary_tools\commands\video_process_and_upload.py", line 18, in run
    VideoProcessCommand().run(args)
  File "C:\Program Files\Python310\lib\site-packages\mapillary_tools\commands\video_process.py", line 31, in run
    ProcessCommand().run(args)
  File "C:\Program Files\Python310\lib\site-packages\mapillary_tools\commands\process.py", line 270, in run
    metadatas = process_geotag_properties(
  File "C:\Program Files\Python310\lib\site-packages\mapillary_tools\process_geotag_properties.py", line 234, in process_geotag_properties
    metadatas.extend(_process_videos_beta(vars_args))
  File "C:\Program Files\Python310\lib\site-packages\mapillary_tools\process_geotag_properties.py", line 295, in _process_videos_beta
    return extractor.process()
  File "C:\Program Files\Python310\lib\site-packages\mapillary_tools\video_data_extraction\extract_video_data.py", line 35, in process
    self._check_paths(paths)
  File "C:\Program Files\Python310\lib\site-packages\mapillary_tools\video_data_extraction\extract_video_data.py", line 134, in _check_paths
    for path in import_paths:
TypeError: 'WindowsPath' object is not iterable
PS I:\Dashcam\2023-07-28>

```

3. exiftool_runtime, explicit pattern:

```
PS I:\Dashcam\2023-07-28>  python.exe -m mapillary_tools.commands --verbose video_process_and_upload --user_name bruno360 --video_geotag_source '{"source": "exiftool_runtime", "pattern": "%f"}' --interpolate_directions 2023_0728_111630_F.MP4 D:/Bilder/tmp/2023_0728_111630_F
2024-01-08 08:13:39,864 - DEBUG   - mapillary_tools version 0.11.1
2024-01-08 08:13:39,864 - DEBUG   - CLI param: verbose: True
2024-01-08 08:13:39,865 - DEBUG   - CLI param: video_import_path: 2023_0728_111630_F.MP4
2024-01-08 08:13:39,865 - DEBUG   - CLI param: import_path: D:\Bilder\tmp\2023_0728_111630_F
2024-01-08 08:13:39,867 - DEBUG   - CLI param: video_sample_distance: 3.0
2024-01-08 08:13:39,867 - DEBUG   - CLI param: video_sample_interval: -1.0
2024-01-08 08:13:39,868 - DEBUG   - CLI param: video_duration_ratio: 1.0
2024-01-08 08:13:39,868 - DEBUG   - CLI param: skip_subfolders: False
2024-01-08 08:13:39,868 - DEBUG   - CLI param: rerun: False
2024-01-08 08:13:39,869 - DEBUG   - CLI param: skip_sample_errors: False
2024-01-08 08:13:39,869 - DEBUG   - CLI param: skip_process_errors: False
2024-01-08 08:13:39,869 - DEBUG   - CLI param: overwrite_all_EXIF_tags: False
2024-01-08 08:13:39,870 - DEBUG   - CLI param: overwrite_EXIF_time_tag: False
2024-01-08 08:13:39,870 - DEBUG   - CLI param: overwrite_EXIF_gps_tag: False
2024-01-08 08:13:39,870 - DEBUG   - CLI param: overwrite_EXIF_direction_tag: False
2024-01-08 08:13:39,871 - DEBUG   - CLI param: overwrite_EXIF_orientation_tag: False
2024-01-08 08:13:39,871 - DEBUG   - CLI param: add_file_name: False
2024-01-08 08:13:39,871 - DEBUG   - CLI param: add_import_date: False
2024-01-08 08:13:39,872 - DEBUG   - CLI param: geotag_source: exif
2024-01-08 08:13:39,872 - DEBUG   - CLI param: video_geotag_source: {source: exiftool_runtime, pattern: %f}
2024-01-08 08:13:39,872 - DEBUG   - CLI param: interpolation_use_gpx_start_time: False
2024-01-08 08:13:39,873 - DEBUG   - CLI param: interpolation_offset_time: 0.0
2024-01-08 08:13:39,873 - DEBUG   - CLI param: offset_angle: 0.0
2024-01-08 08:13:39,873 - DEBUG   - CLI param: offset_time: 0.0
2024-01-08 08:13:39,874 - DEBUG   - CLI param: cutoff_distance: 600.0
2024-01-08 08:13:39,874 - DEBUG   - CLI param: cutoff_time: 60.0
2024-01-08 08:13:39,874 - DEBUG   - CLI param: interpolate_directions: True
2024-01-08 08:13:39,876 - DEBUG   - CLI param: duplicate_distance: 0.1
2024-01-08 08:13:39,877 - DEBUG   - CLI param: duplicate_angle: 5.0
2024-01-08 08:13:39,878 - DEBUG   - CLI param: filetypes: blackvue, camm, gopro, image, video
2024-01-08 08:13:39,878 - DEBUG   - CLI param: user_name: bruno360
2024-01-08 08:13:39,879 - DEBUG   - CLI param: dry_run: False
2024-01-08 08:13:39,881 - INFO    - Extracting video information: ffprobe -print_format json -hide_banner -show_format -show_streams 2023_0728_111630_F.MP4
Input #0, mov,mp4,m4a,3gp,3g2,mj2, from '2023_0728_111630_F.MP4':
  Metadata:
    major_brand     : mp42
    minor_version   : 0
    compatible_brands: isomavc1mp42
    creation_time   : 2023-07-28T11:21:30.000000Z
  Duration: 00:05:00.00, start: 0.000000, bitrate: 33151 kb/s
  Stream #0:0[0x1](eng): Video: hevc (Main) (hvc1 / 0x31637668), yuvj420p(pc, bt709), 3840x2160, 32768 kb/s, 30 fps, 30 tbr, 60k tbn (default)
    Metadata:
      creation_time   : 2023-07-28T11:21:30.000000Z
      handler_name    : VideoHandler
      vendor_id       : [0][0][0][0]
  Stream #0:1[0x2](eng): Audio: aac (LC) (mp4a / 0x6134706D), 16000 Hz, mono, fltp, 64 kb/s (default)
    Metadata:
      creation_time   : 2023-07-28T11:21:30.000000Z
      handler_name    : SoundHandler
      vendor_id       : [0][0][0][0]
2024-01-08 08:13:39,931 - DEBUG   - Extracted video duration: 300.000000
2024-01-08 08:13:39,931 - DEBUG   - Extracted video creation time: 2023-07-28T11:21:30.000000Z
2024-01-08 08:13:39,934 - INFO    - Extracting video metdata
2024-01-08 08:13:39,936 - WARNING - Failed to geotag video 2023_0728_111630_F.MP4: No GPS data found from the video
Traceback (most recent call last):
  File "C:\Program Files\Python310\lib\site-packages\mapillary_tools\geotag\geotag_videos_from_video.py", line 155, in geotag_video
    raise exceptions.MapillaryVideoError("No GPS data found from the video")
mapillary_tools.exceptions.MapillaryVideoError: No GPS data found from the video
2024-01-08 08:13:39,946 - WARNING - No GPS data found from the video
2024-01-08 08:13:39,948 - WARNING - Force the option "filetypes" to be "image" to avoid processing and uploading both the video samples and the videos themselves
2024-01-08 08:13:39,951 - ERROR   - MapillaryBadParameterError: ('Unknown beta source %s or invalid JSON', '{source: exiftool_runtime, pattern: %f}')
Traceback (most recent call last):
  File "C:\Program Files\Python310\lib\site-packages\mapillary_tools\process_geotag_properties.py", line 271, in _process_videos_beta
    parsed_opts = json.loads(source)
  File "C:\Program Files\Python310\lib\json\__init__.py", line 346, in loads
    return _default_decoder.decode(s)
  File "C:\Program Files\Python310\lib\json\decoder.py", line 337, in decode
    obj, end = self.raw_decode(s, idx=_w(s, 0).end())
  File "C:\Program Files\Python310\lib\json\decoder.py", line 353, in raw_decode
    obj, end = self.scan_once(s, idx)
json.decoder.JSONDecodeError: Expecting property name enclosed in double quotes: line 1 column 2 (char 1)

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "C:\Program Files\Python310\lib\site-packages\mapillary_tools\commands\__main__.py", line 168, in main
    args.func(argvars)
  File "C:\Program Files\Python310\lib\site-packages\mapillary_tools\commands\video_process_and_upload.py", line 18, in run
    VideoProcessCommand().run(args)
  File "C:\Program Files\Python310\lib\site-packages\mapillary_tools\commands\video_process.py", line 31, in run
    ProcessCommand().run(args)
  File "C:\Program Files\Python310\lib\site-packages\mapillary_tools\commands\process.py", line 270, in run
    metadatas = process_geotag_properties(
  File "C:\Program Files\Python310\lib\site-packages\mapillary_tools\process_geotag_properties.py", line 234, in process_geotag_properties
    metadatas.extend(_process_videos_beta(vars_args))
  File "C:\Program Files\Python310\lib\site-packages\mapillary_tools\process_geotag_properties.py", line 274, in _process_videos_beta
    raise exceptions.MapillaryBadParameterError(
mapillary_tools.exceptions.MapillaryBadParameterError: ('Unknown beta source %s or invalid JSON', '{source: exiftool_runtime, pattern: %f}')
PS I:\Dashcam\2023-07-28>

```@stefanbj: Hm, seems the JSON snippet needs to be quoted in the windows Power Shell, 
but then it's the same issue as with the implicit pattern examples:

```
PS I:\Dashcam\2023-07-28> python.exe -m mapillary_tools.commands --verbose video_process --video_geotag_source '{\"source\": \"gpx\", \"pattern\": \"%g.gpx\"}' --interpolate_directions 2023_0728_111630_F.MP4 D:/Bilder/tmp/2023_0728_111630_F
2024-01-08 12:38:41,288 - DEBUG   - mapillary_tools version 0.11.1
2024-01-08 12:38:41,288 - DEBUG   - CLI param: verbose: True
2024-01-08 12:38:41,289 - DEBUG   - CLI param: video_import_path: 2023_0728_111630_F.MP4
2024-01-08 12:38:41,289 - DEBUG   - CLI param: import_path: D:\Bilder\tmp\2023_0728_111630_F
2024-01-08 12:38:41,289 - DEBUG   - CLI param: video_sample_distance: 3.0
2024-01-08 12:38:41,289 - DEBUG   - CLI param: video_sample_interval: -1.0
2024-01-08 12:38:41,289 - DEBUG   - CLI param: video_duration_ratio: 1.0
2024-01-08 12:38:41,289 - DEBUG   - CLI param: skip_subfolders: False
2024-01-08 12:38:41,289 - DEBUG   - CLI param: rerun: False
2024-01-08 12:38:41,289 - DEBUG   - CLI param: skip_sample_errors: False
2024-01-08 12:38:41,289 - DEBUG   - CLI param: skip_process_errors: False
2024-01-08 12:38:41,290 - DEBUG   - CLI param: filetypes: blackvue, camm, image, gopro, video
2024-01-08 12:38:41,290 - DEBUG   - CLI param: overwrite_all_EXIF_tags: False
2024-01-08 12:38:41,290 - DEBUG   - CLI param: overwrite_EXIF_time_tag: False
2024-01-08 12:38:41,290 - DEBUG   - CLI param: overwrite_EXIF_gps_tag: False
2024-01-08 12:38:41,290 - DEBUG   - CLI param: overwrite_EXIF_direction_tag: False
2024-01-08 12:38:41,290 - DEBUG   - CLI param: overwrite_EXIF_orientation_tag: False
2024-01-08 12:38:41,290 - DEBUG   - CLI param: add_file_name: False
2024-01-08 12:38:41,290 - DEBUG   - CLI param: add_import_date: False
2024-01-08 12:38:41,290 - DEBUG   - CLI param: geotag_source: exif
2024-01-08 12:38:41,290 - DEBUG   - CLI param: video_geotag_source: {"source": "gpx", "pattern": "%g.gpx"}
2024-01-08 12:38:41,291 - DEBUG   - CLI param: interpolation_use_gpx_start_time: False
2024-01-08 12:38:41,291 - DEBUG   - CLI param: interpolation_offset_time: 0.0
2024-01-08 12:38:41,291 - DEBUG   - CLI param: offset_angle: 0.0
2024-01-08 12:38:41,291 - DEBUG   - CLI param: offset_time: 0.0
2024-01-08 12:38:41,291 - DEBUG   - CLI param: cutoff_distance: 600.0
2024-01-08 12:38:41,291 - DEBUG   - CLI param: cutoff_time: 60.0
2024-01-08 12:38:41,291 - DEBUG   - CLI param: interpolate_directions: True
2024-01-08 12:38:41,291 - DEBUG   - CLI param: duplicate_distance: 0.1
2024-01-08 12:38:41,291 - DEBUG   - CLI param: duplicate_angle: 5.0
2024-01-08 12:38:41,294 - INFO    - Extracting video information: ffprobe -print_format json -hide_banner -show_format -show_streams 2023_0728_111630_F.MP4
Input #0, mov,mp4,m4a,3gp,3g2,mj2, from '2023_0728_111630_F.MP4':
  Metadata:
    major_brand     : mp42
    minor_version   : 0
    compatible_brands: isomavc1mp42
    creation_time   : 2023-07-28T11:21:30.000000Z
  Duration: 00:05:00.00, start: 0.000000, bitrate: 33151 kb/s
  Stream #0:0[0x1](eng): Video: hevc (Main) (hvc1 / 0x31637668), yuvj420p(pc, bt709), 3840x2160, 32768 kb/s, 30 fps, 30 tbr, 60k tbn (default)
    Metadata:
      creation_time   : 2023-07-28T11:21:30.000000Z
      handler_name    : VideoHandler
      vendor_id       : [0][0][0][0]
  Stream #0:1[0x2](eng): Audio: aac (LC) (mp4a / 0x6134706D), 16000 Hz, mono, fltp, 64 kb/s (default)
    Metadata:
      creation_time   : 2023-07-28T11:21:30.000000Z
      handler_name    : SoundHandler
      vendor_id       : [0][0][0][0]
2024-01-08 12:38:41,344 - DEBUG   - Extracted video duration: 300.000000
2024-01-08 12:38:41,344 - DEBUG   - Extracted video creation time: 2023-07-28T11:21:30.000000Z
2024-01-08 12:38:41,347 - INFO    - Extracting video metdata
2024-01-08 12:38:41,350 - WARNING - Failed to geotag video 2023_0728_111630_F.MP4: No GPS data found from the video
Traceback (most recent call last):
  File "C:\Program Files\Python310\lib\site-packages\mapillary_tools\geotag\geotag_videos_from_video.py", line 155, in geotag_video
    raise exceptions.MapillaryVideoError("No GPS data found from the video")
mapillary_tools.exceptions.MapillaryVideoError: No GPS data found from the video
2024-01-08 12:38:41,350 - WARNING - No GPS data found from the video
2024-01-08 12:38:41,351 - WARNING - Force the option "filetypes" to be "image" to avoid processing and uploading both the video samples and the videos themselves
Traceback (most recent call last):
  File "C:\Program Files\Python310\lib\runpy.py", line 196, in _run_module_as_main
    return _run_code(code, main_globals, None,
  File "C:\Program Files\Python310\lib\runpy.py", line 86, in _run_code
    exec(code, run_globals)
  File "C:\Program Files\Python310\lib\site-packages\mapillary_tools\commands\__main__.py", line 177, in <module>
    main()
  File "C:\Program Files\Python310\lib\site-packages\mapillary_tools\commands\__main__.py", line 168, in main
    args.func(argvars)
  File "C:\Program Files\Python310\lib\site-packages\mapillary_tools\commands\video_process.py", line 31, in run
    ProcessCommand().run(args)
  File "C:\Program Files\Python310\lib\site-packages\mapillary_tools\commands\process.py", line 270, in run
    metadatas = process_geotag_properties(
  File "C:\Program Files\Python310\lib\site-packages\mapillary_tools\process_geotag_properties.py", line 234, in process_geotag_properties
    metadatas.extend(_process_videos_beta(vars_args))
  File "C:\Program Files\Python310\lib\site-packages\mapillary_tools\process_geotag_properties.py", line 295, in _process_videos_beta
    return extractor.process()
  File "C:\Program Files\Python310\lib\site-packages\mapillary_tools\video_data_extraction\extract_video_data.py", line 35, in process
    self._check_paths(paths)
  File "C:\Program Files\Python310\lib\site-packages\mapillary_tools\video_data_extraction\extract_video_data.py", line 134, in _check_paths
    for path in import_paths:
TypeError: 'WindowsPath' object is not iterable
PS I:\Dashcam\2023-07-28>

```@stefanbj: Sorry, tried the new interface, was confused by a RTFM issue, accidentally using the 'video_process' instead of the 'process' command;
that stated, I will likely not be the last one making that mistake, and get confused, so might be a good idea to improve the error handling 'video_process' to better handle this type of mistake.@stefanbj: As the new experimental interface works, I guess the feature as such is implemented. The older video_process interface does sort of work, but stills suffers from the time correlation issue, but I think we can consider that a "bug" at this point as the code has been released.

The new interface seems to be a good first step towards simplifying the upload process, shifting the most of the processing to the server-side, so it can be replaced with a web-based interface eventually, at the expense of significantly increasing the amount of data to be uploaded (e.g. from ~250MB to 1.2 GB, so about 5x); and when doing this from locations that does not have the fastes upload speed this can be an issue. However a web-based interface would make it much easier for people to contribute, so I guess overall this might be a good direction to change towards overall.

Though I think I still prefer that older method, as it was usually able to complete the processing and upload in about half the time, OTOH I'm not too keen on going back to the older intervall based sampling - as it has is't issues...

Solution patch:
diff --git a/mapillary_tools/commands/process.py b/mapillary_tools/commands/process.py
index a7f126b..bd1e50b 100644
--- a/mapillary_tools/commands/process.py
+++ b/mapillary_tools/commands/process.py
@@ -20,12 +20,13 @@ class Command:
 
     def add_basic_arguments(self, parser: argparse.ArgumentParser):
         geotag_sources: T.List[GeotagSource] = [
+            "blackvue_videos",
+            "camm",
             "exif",
-            "gpx",
+            "exiftool",
             "gopro_videos",
+            "gpx",
             "nmea",
-            "blackvue_videos",
-            "camm",
         ]
         geotag_gpx_based_sources: T.List[GeotagSource] = [
             "gpx",
diff --git a/mapillary_tools/exceptions.py b/mapillary_tools/exceptions.py
index c0ab9ea..8db839a 100644
--- a/mapillary_tools/exceptions.py
+++ b/mapillary_tools/exceptions.py
@@ -90,6 +90,10 @@ class MapillaryUploadedAlreadyError(_MapillaryDescriptionError):
         self.desc = desc
 
 
+class MapillaryEXIFNotFoundError(_MapillaryDescriptionError):
+    pass
+
+
 class MapillaryUploadConnectionError(MapillaryUserError):
     exit_code = 12
 
diff --git a/mapillary_tools/exiftool_read_video.py b/mapillary_tools/exiftool_read_video.py
new file mode 100644
index 0000000..50553cb
--- /dev/null
+++ b/mapillary_tools/exiftool_read_video.py
@@ -0,0 +1,230 @@
+import typing as T
+import xml.etree.ElementTree as et
+
+from . import exif_read, geo
+
+
+_FIELD_TYPE = T.TypeVar("_FIELD_TYPE", int, float, str, T.List[str])
+MAX_TRACK_ID = 10
+XMP_NAMESPACES: T.Dict[str, str] = {
+    "Keys": "http://ns.exiftool.org/QuickTime/Keys/1.0/",
+    "IFD0": "http://ns.exiftool.org/EXIF/IFD0/1.0/",
+    "QuickTime": "http://ns.exiftool.org/QuickTime/QuickTime/1.0/",
+    "UserData": "http://ns.exiftool.org/QuickTime/UserData/1.0/",
+    "Insta360": "http://ns.exiftool.org/Trailer/Insta360/1.0/",
+    "GoPro": "http://ns.exiftool.org/QuickTime/GoPro/1.0/",
+    **{
+        f"Track{track_id}": f"http://ns.exiftool.org/QuickTime/Track{track_id}/1.0/"
+        for track_id in range(MAX_TRACK_ID)
+    },
+}
+
+
+def maybe_float(text: T.Optional[str]) -> T.Optional[float]:
+    if text is None:
+        return None
+    try:
+        return float(text)
+    except (ValueError, TypeError):
+        return None
+
+
+class ExifToolReadVideo:
+    def __init__(
+        self,
+        etree: et.ElementTree,
+    ) -> None:
+        self.etree = etree
+        self._texts_by_tag: T.Dict[str, T.List[str]] = {}
+        for element in self.etree.getroot():
+            tag = element.tag
+            if element.text is not None:
+                self._texts_by_tag.setdefault(tag, []).append(element.text)
+
+    def extract_gps_track_from_quicktime(self):
+        return self._extract_gps_track_from(
+            "QuickTime:GPSDateTime",
+            "QuickTime:GPSLongitude",
+            "QuickTime:GPSLatitude",
+            "QuickTime:GPSAltitude",
+            "QuickTime:GPSTrack",
+        )
+
+    def extract_gps_track_from_insta360(self):
+        return self._extract_gps_track_from(
+            "Insta360:GPSDateTime",
+            "Insta360:GPSLongitude",
+            "Insta360:GPSLatitude",
+            "Insta360:GPSAltitude",
+            "Insta360:GPSTrack",
+        )
+
+    def extract_gps_track_from_track(self):
+        for track_id in range(MAX_TRACK_ID):
+            track = self._extract_gps_track_from(
+                f"Track{track_id}:GPSDateTime",
+                f"Track{track_id}:GPSLongitude",
+                f"Track{track_id}:GPSLatitude",
+                f"Track{track_id}:GPSAltitude",
+                f"Track{track_id}:GPSTrack",
+            )
+            if track:
+                return track
+        return []
+
+    def extract_gps_track(self):
+        track = self.extract_gps_track_from_quicktime()
+        if track:
+            return track
+
+        track = self.extract_gps_track_from_insta360()
+        if track:
+            return track
+
+        track = self.extract_gps_track_from_track()
+        if track:
+            return track
+
+        return []
+
+    def _extract_gps_track_from(
+        self,
+        time_tag: str,
+        lon_tag: str,
+        lat_tag: str,
+        alt_tag: T.Optional[str] = None,
+        direction_tag: T.Optional[str] = None,
+    ) -> T.List[geo.Point]:
+        gpsdatetimes = [
+            exif_read.parse_gps_datetime(text)
+            for text in self._extract_alternative_fields([time_tag], list) or []
+        ]
+        lons = [
+            maybe_float(lon)
+            for lon in self._extract_alternative_fields([lon_tag], list) or []
+        ]
+        lats = [
+            maybe_float(lat)
+            for lat in self._extract_alternative_fields([lat_tag], list) or []
+        ]
+        if alt_tag is not None:
+            alts = [
+                maybe_float(alt)
+                for alt in self._extract_alternative_fields([alt_tag], list) or []
+            ]
+        else:
+            alts = []
+
+        expected_length = len(gpsdatetimes)
+        if not (len(lats) == expected_length and len(lons) == expected_length):
+            return []
+
+        if direction_tag is not None:
+            directions = [
+                maybe_float(direction)
+                for direction in self._extract_alternative_fields([direction_tag], list)
+                or []
+            ]
+        else:
+            directions = []
+        while len(alts) < expected_length:
+            alts.append(None)
+        while len(directions) < expected_length:
+            directions.append(None)
+
+        track = []
+        for dt, lon, lat, alt, direction in zip(
+            gpsdatetimes, lons, lats, alts, directions
+        ):
+            if dt is None or lon is None or lat is None:
+                continue
+            track.append(
+                geo.Point(
+                    time=geo.as_unix_time(dt),
+                    lon=lon,
+                    lat=lat,
+                    alt=alt,
+                    angle=direction,
+                )
+            )
+        track.sort(key=lambda point: point.time)
+
+        if track:
+            first_time = track[0].time
+            for point in track:
+                point.time = point.time - first_time
+
+        deduplicated_track = []
+        if track:
+            prev = None
+            for point in track:
+                cur = (point.time, point.lon, point.lat)
+                if prev is None or cur != prev:
+                    deduplicated_track.append(point)
+                prev = cur
+
+        return deduplicated_track
+
+    def extract_make(self) -> T.Optional[str]:
+        make = self._extract_alternative_fields(
+            ["IFD0:Make", "Keys:Make", "UserData:Make", "Insta360:Make", "GoPro:Make"],
+            str,
+        )
+        if make is None:
+            return None
+        return make.strip()
+
+    def extract_model(self) -> T.Optional[str]:
+        model = self._extract_alternative_fields(
+            [
+                "IFD0:Model",
+                "Keys:Model",
+                "UserData:Model",
+                "Insta360:Model",
+                "GoPro:Model",
+            ],
+            str,
+        )
+        if model is None:
+            return None
+        return model.strip()
+
+    def _extract_alternative_fields(
+        self,
+        fields: T.Sequence[str],
+        field_type: T.Type[_FIELD_TYPE],
+    ) -> T.Optional[_FIELD_TYPE]:
+        """
+        Extract a value for a list of ordered fields.
+        Return the value of the first existed field in the list
+        """
+        for field in fields:
+            ns, attr_or_tag = field.split(":")
+            values = self._texts_by_tag.get(
+                "{" + XMP_NAMESPACES[ns] + "}" + attr_or_tag
+            )
+            if values is None:
+                continue
+            if field_type is int:
+                value = values[0]
+                try:
+                    return T.cast(_FIELD_TYPE, int(value))
+                except (ValueError, TypeError):
+                    pass
+            elif field_type is float:
+                value = values[0]
+                try:
+                    return T.cast(_FIELD_TYPE, float(value))
+                except (ValueError, TypeError):
+                    pass
+            elif field_type is str:
+                value = values[0]
+                try:
+                    return T.cast(_FIELD_TYPE, str(value))
+                except (ValueError, TypeError):
+                    pass
+            elif field_type is list:
+                return T.cast(_FIELD_TYPE, values)
+            else:
+                raise ValueError(f"Invalid field type {field_type}")
+        return None
diff --git a/mapillary_tools/geotag/geotag_from_exif.py b/mapillary_tools/geotag/geotag_from_exif.py
index 4306abc..fffa773 100644
--- a/mapillary_tools/geotag/geotag_from_exif.py
+++ b/mapillary_tools/geotag/geotag_from_exif.py
@@ -10,7 +10,7 @@ from tqdm import tqdm
 
 from .. import exif_write, geo, types
 from ..exceptions import MapillaryGeoTaggingError
-from ..exif_read import ExifRead
+from ..exif_read import ExifRead, ExifReadABC
 
 from .geotag_from_generic import GeotagFromGeneric
 
@@ -48,9 +48,6 @@ def verify_image_exif_write(
         )
     except Exception as exc:
         # possible error here: struct.error: 'H' format requires 0 <= number <= 65535
-        LOG.warning(
-            "Unknown error test writing image %s", metadata.filename, exc_info=True
-        )
         return types.describe_error_metadata(
             exc,
             metadata.filename,
@@ -65,43 +62,21 @@ class GeotagFromEXIF(GeotagFromGeneric):
         super().__init__()
 
     @staticmethod
-    def geotag_image(
-        image_path: Path, skip_lonlat_error: bool = False
-    ) -> types.ImageMetadataOrError:
-        with image_path.open("rb") as fp:
-            image_data = fp.read()
-        image_bytesio = io.BytesIO(image_data)
-
-        try:
-            exif = ExifRead(image_bytesio)
-        except Exception as ex:
-            LOG.warning(
-                "Unknown error reading EXIF from image %s",
-                image_path,
-                exc_info=True,
-            )
-            return types.describe_error_metadata(
-                ex, image_path, filetype=types.FileType.IMAGE
-            )
-
+    def build_image_metadata(
+        image_path: Path, exif: ExifReadABC, skip_lonlat_error: bool = False
+    ) -> types.ImageMetadata:
         lonlat = exif.extract_lon_lat()
         if lonlat is None:
             if not skip_lonlat_error:
-                exc = MapillaryGeoTaggingError(
+                raise MapillaryGeoTaggingError(
                     "Unable to extract GPS Longitude or GPS Latitude from the image"
                 )
-                return types.describe_error_metadata(
-                    exc, image_path, filetype=types.FileType.IMAGE
-                )
             lonlat = (0.0, 0.0)
         lon, lat = lonlat
 
         capture_time = exif.extract_capture_time()
         if capture_time is None:
-            exc = MapillaryGeoTaggingError("Unable to extract timestamp from the image")
-            return types.describe_error_metadata(
-                exc, image_path, filetype=types.FileType.IMAGE
-            )
+            raise MapillaryGeoTaggingError("Unable to extract timestamp from the image")
 
         image_metadata = types.ImageMetadata(
             filename=image_path,
@@ -118,6 +93,31 @@ class GeotagFromEXIF(GeotagFromGeneric):
             MAPDeviceModel=exif.extract_model(),
         )
 
+        return image_metadata
+
+    @staticmethod
+    def geotag_image(
+        image_path: Path, skip_lonlat_error: bool = False
+    ) -> types.ImageMetadataOrError:
+        with image_path.open("rb") as fp:
+            image_bytesio = io.BytesIO(fp.read())
+
+        try:
+            exif = ExifRead(image_bytesio)
+        except Exception as ex:
+            return types.describe_error_metadata(
+                ex, image_path, filetype=types.FileType.IMAGE
+            )
+
+        try:
+            image_metadata = GeotagFromEXIF.build_image_metadata(
+                image_path, exif, skip_lonlat_error=skip_lonlat_error
+            )
+        except Exception as ex:
+            return types.describe_error_metadata(
+                ex, image_path, filetype=types.FileType.IMAGE
+            )
+
         image_bytesio.seek(0, io.SEEK_SET)
         image_metadata.update_md5sum(image_bytesio)
 
diff --git a/mapillary_tools/geotag/geotag_from_exiftool.py b/mapillary_tools/geotag/geotag_from_exiftool.py
new file mode 100644
index 0000000..adb8ecb
--- /dev/null
+++ b/mapillary_tools/geotag/geotag_from_exiftool.py
@@ -0,0 +1,127 @@
+import io
+import logging
+import typing as T
+import xml.etree.ElementTree as ET
+from multiprocessing import Pool
+from pathlib import Path
+
+from tqdm import tqdm
+
+from .. import exceptions, types, utils
+
+from ..exiftool_read import EXIFTOOL_NAMESPACES, ExifToolRead
+
+from .geotag_from_exif import GeotagFromEXIF, verify_image_exif_write
+from .geotag_from_generic import GeotagFromGeneric
+
+LOG = logging.getLogger(__name__)
+_DESCRIPTION_TAG = "rdf:Description"
+
+
+def canonical_path(path: Path) -> str:
+    return str(path.resolve().as_posix())
+
+
+def find_rdf_description_path(element: ET.Element) -> T.Optional[Path]:
+    about = element.get("{" + EXIFTOOL_NAMESPACES["rdf"] + "}about")
+    if about is None:
+        return None
+    return Path(about)
+
+
+def index_rdf_description_by_path(
+    xml_paths: T.Sequence[Path],
+) -> T.Dict[str, ET.Element]:
+    rdf_description_by_path: T.Dict[str, ET.Element] = {}
+
+    for xml_path in utils.find_xml_files(xml_paths):
+        try:
+            etree = ET.parse(xml_path)
+        except ET.ParseError:
+            continue
+
+        elements = etree.iterfind(_DESCRIPTION_TAG, namespaces=EXIFTOOL_NAMESPACES)
+        for element in elements:
+            path = find_rdf_description_path(element)
+            if path is not None:
+                rdf_description_by_path[canonical_path(path)] = element
+
+    return rdf_description_by_path
+
+
+class GeotagFromExifTool(GeotagFromGeneric):
+    def __init__(self, image_paths: T.Sequence[Path], xml_path: Path):
+        self.image_paths = image_paths
+        self.xml_path = xml_path
+        super().__init__()
+
+    @staticmethod
+    def geotag_image(element: ET.Element) -> types.ImageMetadataOrError:
+        image_path = find_rdf_description_path(element)
+        assert image_path is not None, "must find the path from the element"
+
+        try:
+            exif = ExifToolRead(ET.ElementTree(element))
+        except Exception as ex:
+            return types.describe_error_metadata(
+                ex, image_path, filetype=types.FileType.IMAGE
+            )
+
+        try:
+            image_metadata = GeotagFromEXIF.build_image_metadata(
+                image_path, exif, skip_lonlat_error=False
+            )
+        except Exception as ex:
+            return types.describe_error_metadata(
+                ex, image_path, filetype=types.FileType.IMAGE
+            )
+
+        with image_path.open("rb") as fp:
+            image_bytesio = io.BytesIO(fp.read())
+
+        image_bytesio.seek(0, io.SEEK_SET)
+        image_metadata.update_md5sum(image_bytesio)
+
+        image_bytesio.seek(0, io.SEEK_SET)
+        image_metadata_or_error = verify_image_exif_write(
+            image_metadata,
+            image_data=image_bytesio.read(),
+        )
+
+        return image_metadata_or_error
+
+    def to_description(self) -> T.List[types.ImageMetadataOrError]:
+        rdf_description_by_path = index_rdf_description_by_path([self.xml_path])
+
+        error_metadatas: T.List[types.ErrorMetadata] = []
+        rdf_descriptions: T.List[ET.Element] = []
+        for path in self.image_paths:
+            rdf_description = rdf_description_by_path.get(canonical_path(path))
+            if rdf_description is None:
+                exc = exceptions.MapillaryEXIFNotFoundError(
+                    f"The {_DESCRIPTION_TAG} XML element for the image not found"
+                )
+                error_metadatas.append(
+                    types.describe_error_metadata(
+                        exc, path, filetype=types.FileType.IMAGE
+                    )
+                )
+            else:
+                rdf_descriptions.append(rdf_description)
+
+        with Pool() as pool:
+            image_metadatas = pool.imap(
+                GeotagFromExifTool.geotag_image,
+                rdf_descriptions,
+            )
+            image_metadata_or_errors = list(
+                tqdm(
+                    image_metadatas,
+                    desc="Extracting geotags from images",
+                    unit="images",
+                    disable=LOG.getEffectiveLevel() <= logging.DEBUG,
+                    total=len(self.image_paths),
+                )
+            )
+
+        return error_metadatas + image_metadata_or_errors
diff --git a/mapillary_tools/geotag/geotag_from_exiftool_video.py b/mapillary_tools/geotag/geotag_from_exiftool_video.py
new file mode 100644
index 0000000..ce399c9
--- /dev/null
+++ b/mapillary_tools/geotag/geotag_from_exiftool_video.py
@@ -0,0 +1,109 @@
+import logging
+import typing as T
+import xml.etree.ElementTree as ET
+from multiprocessing import Pool
+from pathlib import Path
+
+from tqdm import tqdm
+
+from .. import exceptions, geo, types
+
+from ..exiftool_read_video import ExifToolReadVideo
+from . import utils as video_utils, geotag_from_exiftool
+from .geotag_from_generic import GeotagFromVideoGeneric
+
+LOG = logging.getLogger(__name__)
+_DESCRIPTION_TAG = "rdf:Description"
+
+
+class GeotagFromExifToolVideo(GeotagFromVideoGeneric):
+    def __init__(self, video_paths: T.Sequence[Path], xml_path: Path):
+        self.video_paths = video_paths
+        self.xml_path = xml_path
+        super().__init__()
+
+    @staticmethod
+    def geotag_video(element: ET.Element) -> types.VideoMetadataOrError:
+        video_path = geotag_from_exiftool.find_rdf_description_path(element)
+        assert video_path is not None, "must find the path from the element"
+
+        try:
+            exif = ExifToolReadVideo(ET.ElementTree(element))
+        except Exception as ex:
+            return types.describe_error_metadata(
+                ex, video_path, filetype=types.FileType.VIDEO
+            )
+
+        points = exif.extract_gps_track()
+        if not points:
+            return types.describe_error_metadata(
+                exceptions.MapillaryGPXEmptyError("Empty GPS data found"),
+                video_path,
+                filetype=types.FileType.VIDEO,
+            )
+
+        video_metadata = types.VideoMetadata(
+            video_path,
+            md5sum=None,
+            filetype=types.FileType.VIDEO,
+            points=points,
+            make=exif.extract_make(),
+            model=exif.extract_model(),
+        )
+
+        stationary = video_utils.is_video_stationary(
+            geo.get_max_distance_from_start(
+                [(p.lat, p.lon) for p in video_metadata.points]
+            )
+        )
+        if stationary:
+            return types.describe_error_metadata(
+                exceptions.MapillaryStationaryVideoError("Stationary video"),
+                video_metadata.filename,
+                filetype=video_metadata.filetype,
+            )
+
+        LOG.debug("Calculating MD5 checksum for %s", str(video_metadata.filename))
+        video_metadata.update_md5sum()
+
+        return video_metadata
+
+    def to_description(self) -> T.List[types.VideoMetadataOrError]:
+        rdf_description_by_path = geotag_from_exiftool.index_rdf_description_by_path(
+            [self.xml_path]
+        )
+
+        error_metadatas: T.List[types.ErrorMetadata] = []
+        rdf_descriptions: T.List[ET.Element] = []
+        for path in self.video_paths:
+            rdf_description = rdf_description_by_path.get(
+                geotag_from_exiftool.canonical_path(path)
+            )
+            if rdf_description is None:
+                exc = exceptions.MapillaryEXIFNotFoundError(
+                    f"The {_DESCRIPTION_TAG} XML element for the video not found"
+                )
+                error_metadatas.append(
+                    types.describe_error_metadata(
+                        exc, path, filetype=types.FileType.VIDEO
+                    )
+                )
+            else:
+                rdf_descriptions.append(rdf_description)
+
+        with Pool() as pool:
+            video_metadatas = pool.imap(
+                GeotagFromExifToolVideo.geotag_video,
+                rdf_descriptions,
+            )
+            video_metadata_or_errors = list(
+                tqdm(
+                    video_metadatas,
+                    desc="Extracting GPS tracks from videos",
+                    unit="videos",
+                    disable=LOG.getEffectiveLevel() <= logging.DEBUG,
+                    total=len(self.video_paths),
+                )
+            )
+
+        return error_metadatas + video_metadata_or_errors
diff --git a/mapillary_tools/geotag/geotag_from_generic.py b/mapillary_tools/geotag/geotag_from_generic.py
index 3d5ed44..85343fd 100644
--- a/mapillary_tools/geotag/geotag_from_generic.py
+++ b/mapillary_tools/geotag/geotag_from_generic.py
@@ -9,3 +9,11 @@ class GeotagFromGeneric:
 
     def to_description(self) -> T.List[types.ImageMetadataOrError]:
         return []
+
+
+class GeotagFromVideoGeneric:
+    def __init__(self) -> None:
+        pass
+
+    def to_description(self) -> T.List[types.VideoMetadataOrError]:
+        return []
diff --git a/mapillary_tools/geotag/geotag_from_gpx.py b/mapillary_tools/geotag/geotag_from_gpx.py
index 1f448ce..0e0cadf 100644
--- a/mapillary_tools/geotag/geotag_from_gpx.py
+++ b/mapillary_tools/geotag/geotag_from_gpx.py
@@ -37,7 +37,7 @@ class GeotagFromGPX(GeotagFromGeneric):
         self, image_paths: T.Sequence[Path]
     ) -> T.List[types.ImageMetadataOrError]:
         with Pool() as pool:
-            return pool.map(self.geotag_image, image_paths)
+            return pool.map(GeotagFromGPX.geotag_image, image_paths)
 
     def _interpolate_image_metadata_along(
         self,
@@ -188,7 +188,7 @@ class GeotagFromGPXWithProgress(GeotagFromGPX):
 
         output = []
         with Pool() as pool:
-            iter = pool.imap(self.geotag_image, image_paths)
+            iter = pool.imap(GeotagFromGPX.geotag_image, image_paths)
             for image_metadata_or_error in iter:
                 self._progress_bar.update(1)
                 output.append(image_metadata_or_error)
diff --git a/mapillary_tools/geotag/geotag_from_video.py b/mapillary_tools/geotag/geotag_from_video.py
index afb61a7..bb6de7d 100644
--- a/mapillary_tools/geotag/geotag_from_video.py
+++ b/mapillary_tools/geotag/geotag_from_video.py
@@ -15,11 +15,12 @@ from . import (
     simple_mp4_parser as parser,
     utils as video_utils,
 )
+from .geotag_from_generic import GeotagFromVideoGeneric
 
 LOG = logging.getLogger(__name__)
 
 
-class GeotagFromVideo:
+class GeotagFromVideo(GeotagFromVideoGeneric):
     def __init__(
         self,
         video_paths: T.Sequence[Path],
@@ -28,7 +29,7 @@ class GeotagFromVideo:
         self.video_paths = video_paths
         self.filetypes = filetypes
 
-    def to_descriptions(self) -> T.List[types.VideoMetadataOrError]:
+    def to_description(self) -> T.List[types.VideoMetadataOrError]:
         with Pool() as pool:
             video_metadatas = pool.imap(
                 self._geotag_video,
diff --git a/mapillary_tools/process_geotag_properties.py b/mapillary_tools/process_geotag_properties.py
index 0bc0ec5..cc1a9f3 100644
--- a/mapillary_tools/process_geotag_properties.py
+++ b/mapillary_tools/process_geotag_properties.py
@@ -18,6 +18,8 @@ from .geotag import (
     geotag_from_blackvue,
     geotag_from_camm,
     geotag_from_exif,
+    geotag_from_exiftool,
+    geotag_from_exiftool_video,
     geotag_from_generic,
     geotag_from_gopro,
     geotag_from_gpx_file,
@@ -30,7 +32,9 @@ from .types import FileType
 LOG = logging.getLogger(__name__)
 
 
-GeotagSource = Literal["gopro_videos", "blackvue_videos", "camm", "exif", "gpx", "nmea"]
+GeotagSource = Literal[
+    "gopro_videos", "blackvue_videos", "camm", "exif", "gpx", "nmea", "exiftool"
+]
 
 
 def _process_images(
@@ -63,10 +67,16 @@ def _process_images(
             raise exceptions.MapillaryFileNotFoundError(
                 "Geotag source path (--geotag_source_path) is required"
             )
-        if not geotag_source_path.is_file():
-            raise exceptions.MapillaryFileNotFoundError(
-                f"Geotag source file not found: {geotag_source_path}"
-            )
+        if geotag_source == "exiftool":
+            if not geotag_source_path.exists():
+                raise exceptions.MapillaryFileNotFoundError(
+                    f"Geotag source file not found: {geotag_source_path}"
+                )
+        else:
+            if not geotag_source_path.is_file():
+                raise exceptions.MapillaryFileNotFoundError(
+                    f"Geotag source file not found: {geotag_source_path}"
+                )
 
         if geotag_source == "gpx":
             geotag = geotag_from_gpx_file.GeotagFromGPXFile(
@@ -100,6 +110,11 @@ def _process_images(
                 utils.find_videos([geotag_source_path]),
                 offset_time=interpolation_offset_time,
             )
+        elif geotag_source == "exiftool":
+            geotag = geotag_from_exiftool.GeotagFromExifTool(
+                image_paths,
+                geotag_source_path,
+            )
         else:
             raise RuntimeError(f"Invalid geotag source {geotag_source}")
 
@@ -166,14 +181,29 @@ def process_geotag_properties(
         FileType.CAMM in filetypes
         or FileType.GOPRO in filetypes
         or FileType.BLACKVUE in filetypes
+        or FileType.VIDEO in filetypes
     ):
         video_paths = utils.find_videos(
             import_paths,
             skip_subfolders=skip_subfolders,
             check_file_suffix=check_file_suffix,
         )
-        geotag = geotag_from_video.GeotagFromVideo(video_paths, filetypes=filetypes)
-        metadatas.extend(geotag.to_descriptions())
+        geotag: geotag_from_generic.GeotagFromVideoGeneric
+        if geotag_source == "exiftool":
+            if geotag_source_path is None:
+                raise exceptions.MapillaryFileNotFoundError(
+                    "Geotag source path (--geotag_source_path) is required"
+                )
+            if not geotag_source_path.exists():
+                raise exceptions.MapillaryFileNotFoundError(
+                    f"Geotag source file not found: {geotag_source_path}"
+                )
+            geotag = geotag_from_exiftool_video.GeotagFromExifToolVideo(
+                video_paths, geotag_source_path
+            )
+        else:
+            geotag = geotag_from_video.GeotagFromVideo(video_paths, filetypes=filetypes)
+        metadatas.extend(geotag.to_description())
 
     # filenames should be deduplicated in utils.find_images/utils.find_videos
     assert len(metadatas) == len(
diff --git a/mapillary_tools/types.py b/mapillary_tools/types.py
index 1baa6d8..699b4f6 100644
--- a/mapillary_tools/types.py
+++ b/mapillary_tools/types.py
@@ -40,6 +40,7 @@ class FileType(enum.Enum):
     CAMM = "camm"
     GOPRO = "gopro"
     IMAGE = "image"
+    VIDEO = "video"
 
 
 @dataclasses.dataclass
@@ -404,6 +405,7 @@ VideoDescriptionFileSchema = merge_schema(
                     FileType.CAMM.value,
                     FileType.GOPRO.value,
                     FileType.BLACKVUE.value,
+                    FileType.VIDEO.value,
                 ],
                 "description": "The video file type",
             },
diff --git a/mapillary_tools/utils.py b/mapillary_tools/utils.py
index d87ca0c..f38a7ff 100644
--- a/mapillary_tools/utils.py
+++ b/mapillary_tools/utils.py
@@ -144,3 +144,25 @@ def find_zipfiles(
             else:
                 zip_paths.append(path)
     return list(deduplicate_paths(zip_paths))
+
+
+def find_xml_files(
+    import_paths: T.Sequence[Path],
+    skip_subfolders: bool = False,
+    check_file_suffix: bool = False,
+) -> T.List[Path]:
+    xml_paths: T.List[Path] = []
+    for path in import_paths:
+        if path.is_dir():
+            xml_paths.extend(
+                file
+                for file in iterate_files(path, not skip_subfolders)
+                if file.suffix.lower() in [".xml"]
+            )
+        else:
+            if check_file_suffix:
+                if path.suffix.lower() in [".xml"]:
+                    xml_paths.append(path)
+            else:
+                xml_paths.append(path)
+    return list(deduplicate_paths(xml_paths))
diff --git a/schema/image_description_schema.json b/schema/image_description_schema.json
index 0b4d25f..d26d540 100644
--- a/schema/image_description_schema.json
+++ b/schema/image_description_schema.json
@@ -62,7 +62,8 @@
                     "enum": [
                         "camm",
                         "gopro",
-                        "blackvue"
+                        "blackvue",
+                        "video"
                     ],
                     "description": "The video file type"
                 }


Test patch:
diff --git a/tests/unit/test_sequence_processing.py b/tests/unit/test_sequence_processing.py
index 9d443b4..92db5c6 100644
--- a/tests/unit/test_sequence_processing.py
+++ b/tests/unit/test_sequence_processing.py
@@ -360,7 +360,7 @@ def test_process_finalize(setup_data):
         {
             "error": {
                 "type": "ValidationError",
-                "message": "'image' is not one of ['camm', 'gopro', 'blackvue']",
+                "message": "'image' is not one of ['camm', 'gopro', 'blackvue', 'video']",
             },
             "filename": str(setup_data.join("test_video.mp4")),
             "filetype": "image",