File Coverage

blib/lib/Image/ExifTool/QuickTimeStream.pl
Criterion Covered Total %
statement 121 1223 9.8
branch 53 822 6.4
condition 29 335 8.6
subroutine 8 28 28.5
pod 0 25 0.0
total 211 2433 8.6


line stmt bran cond sub pod time code
1             #------------------------------------------------------------------------------
2             # File: QuickTimeStream.pl
3             #
4             # Description: Extract embedded information from QuickTime media data
5             #
6             # Revisions: 2018-01-03 - P. Harvey Created
7             #
8             # References: 1) https://developer.apple.com/library/content/documentation/QuickTime/QTFF/QTFFChap3/qtff3.html#//apple_ref/doc/uid/TP40000939-CH205-SW130
9             # 2) http://sergei.nz/files/nvtk_mp42gpx.py
10             # 3) https://forum.flitsservice.nl/dashcam-info/dod-ls460w-gps-data-uit-mov-bestand-lezen-t87926.html
11             # 4) https://developers.google.com/streetview/publish/camm-spec
12             # 5) https://sergei.nz/extracting-gps-data-from-viofo-a119-and-other-novatek-powered-cameras/
13             # 6) Thomas Allen https://github.com/exiftool/exiftool/pull/62
14             #------------------------------------------------------------------------------
15             package Image::ExifTool::QuickTime;
16              
17 2     2   17 use strict;
  2         6  
  2         104  
18              
19 2     2   16 use Image::ExifTool qw(:DataAccess :Utils);
  2         3  
  2         553  
20 2     2   20 use Image::ExifTool::QuickTime;
  2         4  
  2         36885  
21              
22             sub Process_tx3g($$$);
23             sub Process_marl($$$);
24             sub Process_mebx($$$);
25             sub ProcessFreeGPS($$$);
26             sub ProcessFreeGPS2($$$);
27             sub Process360Fly($$$);
28             sub ProcessFMAS($$$);
29              
30             my $debug; # set to 1 for extra debugging messages
31              
32             # QuickTime data types that have ExifTool equivalents
33             # (ref https://developer.apple.com/library/content/documentation/QuickTime/QTFF/Metadata/Metadata.html#//apple_ref/doc/uid/TP40000939-CH1-SW35)
34             my %qtFmt = (
35             0 => 'undef',
36             1 => 'string', # (UTF-8)
37             # 2 - UTF-16
38             # 3 - shift-JIS
39             # 4 - UTF-8 sort
40             # 5 - UTF-16 sort
41             # 13 - JPEG image
42             # 14 - PNG image
43             # 21 - signed integer (1,2,3 or 4 bytes)
44             # 22 - unsigned integer (1,2,3 or 4 bytes)
45             23 => 'float',
46             24 => 'double',
47             # 27 - BMP image
48             # 28 - QuickTime atom
49             65 => 'int8s',
50             66 => 'int16s',
51             67 => 'int32s',
52             70 => 'float', # float[2] x,y
53             71 => 'float', # float[2] width,height
54             72 => 'float', # float[4] x,y,width,height
55             74 => 'int64s',
56             75 => 'int8u',
57             76 => 'int16u',
58             77 => 'int32u',
59             78 => 'int64u',
60             79 => 'float', # float[9] transform matrix
61             80 => 'float', # float[8] face coordinates
62             );
63              
64             # maximums for validating H,M,S,d,m,Y from "freeGPS " metadata
65             my @dateMax = ( 24, 59, 59, 2200, 12, 31 );
66              
67             # typical (minimum?) size of freeGPS block
68             my $gpsBlockSize = 0x8000;
69              
70             # conversion factors
71             my $knotsToKph = 1.852; # knots --> km/h
72             my $mpsToKph = 3.6; # m/s --> km/h
73             my $mphToKph = 1.60934; # mph --> km/h
74              
75             # handler types to process based on MetaFormat/OtherFormat
76             my %processByMetaFormat = (
77             meta => 1, # ('CTMD' in CR3 images, 'priv' unknown in DJI video)
78             data => 1, # ('RVMI')
79             sbtl => 1, # (subtitle; 'tx3g' in Yuneec drone videos)
80             ctbx => 1, # ('marl' in GM videos)
81             );
82              
83             # data lengths for each INSV record type
84             my %insvDataLen = (
85             0x300 => 0, # accelerometer (could be either 20 or 56 bytes)
86             0x400 => 16, # exposure (ref 6)
87             0x600 => 8, # timestamps (ref 6)
88             0x700 => 53, # GPS
89             );
90              
91             # limit the default amount of data we read for some record types
92             # (to avoid running out of memory)
93             my %insvLimit = (
94             0x300 => [ 'accelerometer', 20000 ], # maximum of 20000 accelerometer records
95             );
96              
97             # tags extracted from various QuickTime data streams
98             %Image::ExifTool::QuickTime::Stream = (
99             GROUPS => { 2 => 'Location' },
100             NOTES => q{
101             The tags below are extracted from timed metadata in QuickTime and other
102             formats of video files when the ExtractEmbedded option is used. Although
103             most of these tags are combined into the single table below, ExifTool
104             currently reads 62 different formats of timed GPS metadata from video files.
105             },
106             VARS => { NO_ID => 1 },
107             GPSLatitude => { PrintConv => 'Image::ExifTool::GPS::ToDMS($self, $val, 1, "N")', RawConv => '$$self{FoundGPSLatitude} = 1; $val' },
108             GPSLongitude => { PrintConv => 'Image::ExifTool::GPS::ToDMS($self, $val, 1, "E")' },
109             GPSAltitude => { PrintConv => '(sprintf("%.4f", $val) + 0) . " m"' }, # round to 4 decimals
110             GPSSpeed => { PrintConv => 'sprintf("%.4f", $val) + 0', Notes => 'in km/h unless GPSSpeedRef says otherwise' },
111             GPSSpeedRef => { PrintConv => { K => 'km/h', M => 'mph', N => 'knots' } },
112             GPSTrack => { PrintConv => 'sprintf("%.4f", $val) + 0', Notes => 'relative to true north unless GPSTrackRef says otherwise' },
113             GPSTrackRef => { PrintConv => { M => 'Magnetic North', T => 'True North' } },
114             GPSDateTime => {
115             Groups => { 2 => 'Time' },
116             Description => 'GPS Date/Time',
117             RawConv => '$$self{FoundGPSDateTime} = 1; $val',
118             PrintConv => '$self->ConvertDateTime($val)',
119             },
120             GPSTimeStamp => { PrintConv => 'Image::ExifTool::GPS::PrintTimeStamp($val)', Groups => { 2 => 'Time' } },
121             GPSSatellites=> { },
122             GPSDOP => { Description => 'GPS Dilution Of Precision' },
123             Distance => { PrintConv => '"$val m"' },
124             VerticalSpeed=> { PrintConv => '"$val m/s"' },
125             FNumber => { PrintConv => 'Image::ExifTool::Exif::PrintFNumber($val)', Groups => { 2 => 'Camera' } },
126             ExposureTime => { PrintConv => 'Image::ExifTool::Exif::PrintExposureTime($val)', Groups => { 2 => 'Camera' } },
127             ExposureCompensation => { PrintConv => 'Image::ExifTool::Exif::PrintFraction($val)', Groups => { 2 => 'Camera' } },
128             ISO => { Groups => { 2 => 'Camera' } },
129             CameraDateTime=>{ PrintConv => '$self->ConvertDateTime($val)', Groups => { 2 => 'Time' } },
130             VideoTimeStamp => { Groups => { 2 => 'Video' } },
131             Accelerometer=> { Notes => '3-axis acceleration in units of g' },
132             AccelerometerData => { },
133             AngularVelocity => { },
134             GSensor => { },
135             Car => { },
136             RawGSensor => {
137             # (same as GSensor, but offset by some unknown value)
138             ValueConv => 'my @a=split " ",$val; $_/=1000 foreach @a; "@a"',
139             },
140             Text => { Groups => { 2 => 'Other' } },
141             TimeCode => { Groups => { 2 => 'Video' } },
142             FrameNumber => { Groups => { 2 => 'Video' } },
143             SampleTime => { Groups => { 2 => 'Video' }, PrintConv => 'ConvertDuration($val)', Notes => 'sample decoding time' },
144             SampleDuration=>{ Groups => { 2 => 'Video' }, PrintConv => 'ConvertDuration($val)' },
145             UserLabel => { Groups => { 2 => 'Other' } },
146             KiloCalories => { Groups => { 2 => 'Other' } },
147             SampleDateTime => {
148             Groups => { 2 => 'Time' },
149             ValueConv => q{
150             my $str = ConvertUnixTime($val);
151             my $frac = $val - int($val);
152             if ($frac != 0) {
153             $frac = sprintf('%.6f', $frac);
154             $frac =~ s/^0//;
155             $frac =~ s/0+$//;
156             $str .= $frac;
157             }
158             return $str;
159             },
160             PrintConv => '$self->ConvertDateTime($val)',
161             },
162             #
163             # timed metadata decoded based on MetaFormat (format of 'meta' or 'data' sample description)
164             # [or HandlerType, or specific 'vide' type if specified]
165             #
166             mebx => {
167             Name => 'mebx',
168             SubDirectory => {
169             TagTable => 'Image::ExifTool::QuickTime::Keys',
170             ProcessProc => \&Process_mebx,
171             },
172             },
173             gpmd => [{
174             Name => 'gpmd_Kingslim', # Kingslim D4 dashcam
175             Condition => '$$valPt =~ /^.{21}\0\0\0A[NS][EW]/s',
176             SubDirectory => {
177             TagTable => 'Image::ExifTool::QuickTime::Stream',
178             ProcessProc => \&ProcessFreeGPS,
179             },
180             },{
181             Name => 'gpmd_Rove', # Rove Stealth 4K encrypted text
182             Condition => '$$valPt =~ /^\0\0\xf2\xe1\xf0\xeeTT/',
183             SubDirectory => {
184             TagTable => 'Image::ExifTool::QuickTime::Stream',
185             ProcessProc => \&Process_text,
186             },
187             },{
188             Name => 'gpmd_FMAS', # Vantrue N2S binary format
189             Condition => '$$valPt =~ /^FMAS\0\0\0\0/',
190             SubDirectory => {
191             TagTable => 'Image::ExifTool::QuickTime::Stream',
192             ProcessProc => \&ProcessFMAS,
193             },
194             },{
195             Name => 'gpmd_GoPro',
196             SubDirectory => { TagTable => 'Image::ExifTool::GoPro::GPMF' },
197             }],
198             fdsc => {
199             Name => 'fdsc',
200             Condition => '$$valPt =~ /^GPRO/',
201             # (other types of "fdsc" samples aren't yet parsed: /^GP\x00/ and /^GP\x04/)
202             SubDirectory => { TagTable => 'Image::ExifTool::GoPro::fdsc' },
203             },
204             rtmd => {
205             Name => 'rtmd',
206             SubDirectory => { TagTable => 'Image::ExifTool::Sony::rtmd' },
207             },
208             marl => {
209             Name => 'marl',
210             SubDirectory => { TagTable => 'Image::ExifTool::QuickTime::marl' },
211             },
212             CTMD => { # (Canon Timed MetaData)
213             Name => 'CTMD',
214             SubDirectory => { TagTable => 'Image::ExifTool::Canon::CTMD' },
215             },
216             tx3g => {
217             Name => 'tx3g',
218             SubDirectory => { TagTable => 'Image::ExifTool::QuickTime::tx3g' },
219             },
220             RVMI => [{ # data "OtherFormat" written by unknown software
221             Name => 'RVMI_gReV',
222             Condition => '$$valPt =~ /^gReV/', # GPS data
223             SubDirectory => {
224             TagTable => 'Image::ExifTool::QuickTime::RVMI_gReV',
225             ByteOrder => 'Little-endian',
226             },
227             },{
228             Name => 'RVMI_sReV',
229             Condition => '$$valPt =~ /^sReV/', # sensor data
230             SubDirectory => {
231             TagTable => 'Image::ExifTool::QuickTime::RVMI_sReV',
232             ByteOrder => 'Little-endian',
233             },
234             # (there is also "tReV" data that hasn't been decoded yet)
235             }],
236             camm => [{
237             Name => 'camm0',
238             # (according to the spec. the first 2 bytes are reserved and should be zero,
239             # but I have a sample where these bytes are non-zero, so allow anything here)
240             Condition => '$$valPt =~ /^..\0\0/s',
241             SubDirectory => {
242             TagTable => 'Image::ExifTool::QuickTime::camm0',
243             ByteOrder => 'Little-Endian',
244             },
245             },{
246             Name => 'camm1',
247             Condition => '$$valPt =~ /^..\x01\0/s',
248             SubDirectory => {
249             TagTable => 'Image::ExifTool::QuickTime::camm1',
250             ByteOrder => 'Little-Endian',
251             },
252             },{ # (written by Insta360) - [HandlerType, not MetaFormat]
253             Name => 'camm2',
254             Condition => '$$valPt =~ /^..\x02\0/s',
255             SubDirectory => {
256             TagTable => 'Image::ExifTool::QuickTime::camm2',
257             ByteOrder => 'Little-Endian',
258             },
259             },{
260             Name => 'camm3',
261             Condition => '$$valPt =~ /^..\x03\0/s',
262             SubDirectory => {
263             TagTable => 'Image::ExifTool::QuickTime::camm3',
264             ByteOrder => 'Little-Endian',
265             },
266             },{
267             Name => 'camm4',
268             Condition => '$$valPt =~ /^..\x04\0/s',
269             SubDirectory => {
270             TagTable => 'Image::ExifTool::QuickTime::camm4',
271             ByteOrder => 'Little-Endian',
272             },
273             },{
274             Name => 'camm5',
275             Condition => '$$valPt =~ /^..\x05\0/s',
276             SubDirectory => {
277             TagTable => 'Image::ExifTool::QuickTime::camm5',
278             ByteOrder => 'Little-Endian',
279             },
280             },{
281             Name => 'camm6',
282             Condition => '$$valPt =~ /^..\x06\0/s',
283             SubDirectory => {
284             TagTable => 'Image::ExifTool::QuickTime::camm6',
285             ByteOrder => 'Little-Endian',
286             },
287             },{
288             Name => 'camm7',
289             Condition => '$$valPt =~ /^..\x07\0/s',
290             SubDirectory => {
291             TagTable => 'Image::ExifTool::QuickTime::camm7',
292             ByteOrder => 'Little-Endian',
293             },
294             }],
295             mett => { # Parrot drones
296             Name => 'mett',
297             SubDirectory => { TagTable => 'Image::ExifTool::Parrot::mett' },
298             },
299             JPEG => { # (in CR3 images) - [vide HandlerType with JPEG in SampleDescription, not MetaFormat]
300             Name => 'JpgFromRaw',
301             Groups => { 2 => 'Preview' },
302             RawConv => '$self->ValidateImage(\$val,$tag)',
303             },
304             text => { # (TomTom Bandit MP4) - [sbtl HandlerType with 'text' in SampleDescription]
305             Name => 'PreviewInfo',
306             Condition => 'length $$valPt > 12 and Get32u($valPt,4) == length($$valPt) and $$valPt =~ /^.{8}\xff\xd8\xff/s',
307             SubDirectory => { TagTable => 'Image::ExifTool::QuickTime::PreviewInfo' },
308             },
309             INSV => {
310             Groups => { 0 => 'Trailer', 1 => 'Insta360' }, # (so these groups will appear in the -listg options)
311             SubDirectory => { TagTable => 'Image::ExifTool::QuickTime::INSV_MakerNotes' },
312             },
313             Unknown00 => { Unknown => 1 },
314             Unknown01 => { Unknown => 1 },
315             Unknown02 => { Unknown => 1 },
316             Unknown03 => { Unknown => 1 },
317             );
318              
319             # tags found in 'camm' type 0 timed metadata (ref 4)
320             %Image::ExifTool::QuickTime::camm0 = (
321             PROCESS_PROC => \&Image::ExifTool::ProcessBinaryData,
322             GROUPS => { 2 => 'Location' },
323             FIRST_ENTRY => 0,
324             NOTES => q{
325             The camm0 through camm7 tables define tags extracted from the Google Street
326             View Camera Motion Metadata of MP4 videos. See
327             L for the
328             specification.
329             },
330             4 => {
331             Name => 'AngleAxis',
332             Notes => 'angle axis orientation in radians in local coordinate system',
333             Format => 'float[3]',
334             },
335             );
336              
337             # tags found in 'camm' type 1 timed metadata (ref 4)
338             %Image::ExifTool::QuickTime::camm1 = (
339             PROCESS_PROC => \&Image::ExifTool::ProcessBinaryData,
340             GROUPS => { 2 => 'Camera' },
341             FIRST_ENTRY => 0,
342             4 => {
343             Name => 'PixelExposureTime',
344             Format => 'int32s',
345             ValueConv => '$val * 1e-9',
346             PrintConv => 'sprintf("%.4g ms", $val * 1000)',
347             },
348             8 => {
349             Name => 'RollingShutterSkewTime',
350             Format => 'int32s',
351             ValueConv => '$val * 1e-9',
352             PrintConv => 'sprintf("%.4g ms", $val * 1000)',
353             },
354             );
355              
356             # tags found in 'camm' type 2 timed metadata (ref PH, Insta360Pro)
357             %Image::ExifTool::QuickTime::camm2 = (
358             PROCESS_PROC => \&Image::ExifTool::ProcessBinaryData,
359             GROUPS => { 2 => 'Location' },
360             FIRST_ENTRY => 0,
361             4 => {
362             Name => 'AngularVelocity',
363             Notes => 'gyro angular velocity about X, Y and Z axes in rad/s',
364             Format => 'float[3]',
365             },
366             );
367              
368             # tags found in 'camm' type 3 timed metadata (ref PH, Insta360Pro)
369             %Image::ExifTool::QuickTime::camm3 = (
370             PROCESS_PROC => \&Image::ExifTool::ProcessBinaryData,
371             GROUPS => { 2 => 'Location' },
372             FIRST_ENTRY => 0,
373             4 => {
374             Name => 'Acceleration',
375             Notes => 'acceleration in the X, Y and Z directions in m/s^2',
376             Format => 'float[3]',
377             },
378             );
379              
380             # tags found in 'camm' type 4 timed metadata (ref 4)
381             %Image::ExifTool::QuickTime::camm4 = (
382             PROCESS_PROC => \&Image::ExifTool::ProcessBinaryData,
383             GROUPS => { 2 => 'Location' },
384             FIRST_ENTRY => 0,
385             4 => {
386             Name => 'Position',
387             Notes => 'X, Y, Z position in local coordinate system',
388             Format => 'float[3]',
389             },
390             );
391              
392             # tags found in 'camm' type 5 timed metadata (ref 4)
393             %Image::ExifTool::QuickTime::camm5 = (
394             PROCESS_PROC => \&Image::ExifTool::ProcessBinaryData,
395             GROUPS => { 2 => 'Location' },
396             FIRST_ENTRY => 0,
397             4 => {
398             Name => 'GPSLatitude',
399             Format => 'double',
400             RawConv => '$$self{FoundGPSLatitude} = 1; $val',
401             ValueConv => 'Image::ExifTool::GPS::ToDegrees($val, 1)',
402             PrintConv => 'Image::ExifTool::GPS::ToDMS($self, $val, 1, "N")',
403             },
404             12 => {
405             Name => 'GPSLongitude',
406             Format => 'double',
407             ValueConv => 'Image::ExifTool::GPS::ToDegrees($val, 1)',
408             PrintConv => 'Image::ExifTool::GPS::ToDMS($self, $val, 1, "E")',
409             },
410             20 => {
411             Name => 'GPSAltitude',
412             Format => 'double',
413             PrintConv => '$_ = sprintf("%.6f", $val); s/\.?0+$//; "$_ m"',
414             },
415             );
416              
417             # tags found in 'camm' type 6 timed metadata (ref PH/4, Insta360)
418             %Image::ExifTool::QuickTime::camm6 = (
419             PROCESS_PROC => \&Image::ExifTool::ProcessBinaryData,
420             GROUPS => { 2 => 'Location' },
421             FIRST_ENTRY => 0,
422             0x04 => {
423             Name => 'GPSDateTime',
424             Description => 'GPS Date/Time',
425             Groups => { 2 => 'Time' },
426             Format => 'double',
427             RawConv => '$$self{FoundGPSDateTime} = 1; $val',
428             # by the specification, this should use the GPS epoch of Jan 6, 1980,
429             # but I have samples which use the Unix epoch of Jan 1, 1970, so convert
430             # to the Unix Epoch only if it doesn't match the CreateDate within 5 years
431             ValueConv => q{
432             my $offset = 315964800;
433             if ($$self{CreateDate} and $$self{CreateDate} - $val > 24 * 3600 * 365 * 5) {
434             $val += $offset;
435             }
436             my $str = ConvertUnixTime($val);
437             my $frac = $val - int($val);
438             if ($frac != 0) {
439             $frac = sprintf('%.6f', $frac);
440             $frac =~ s/^0//;
441             $frac =~ s/0+$//;
442             $str .= $frac;
443             }
444             return $str . 'Z';
445             },
446             PrintConv => '$self->ConvertDateTime($val)',
447             },
448             0x0c => {
449             Name => 'GPSMeasureMode',
450             Format => 'int32u',
451             PrintConv => {
452             0 => 'No Measurement',
453             2 => '2-Dimensional Measurement',
454             3 => '3-Dimensional Measurement',
455             },
456             },
457             0x10 => {
458             Name => 'GPSLatitude',
459             Format => 'double',
460             RawConv => '$$self{FoundGPSLatitude} = 1; $val',
461             ValueConv => 'Image::ExifTool::GPS::ToDegrees($val, 1)',
462             PrintConv => 'Image::ExifTool::GPS::ToDMS($self, $val, 1, "N")',
463             },
464             0x18 => {
465             Name => 'GPSLongitude',
466             Format => 'double',
467             ValueConv => 'Image::ExifTool::GPS::ToDegrees($val, 1)',
468             PrintConv => 'Image::ExifTool::GPS::ToDMS($self, $val, 1, "E")',
469             },
470             0x20 => {
471             Name => 'GPSAltitude',
472             Format => 'float',
473             PrintConv => '$_ = sprintf("%.3f", $val); s/\.?0+$//; "$_ m"',
474             },
475             0x24 => { Name => 'GPSHorizontalAccuracy', Format => 'float', Notes => 'metres' },
476             0x28 => { Name => 'GPSVerticalAccuracy', Format => 'float' },
477             0x2c => { Name => 'GPSVelocityEast', Format => 'float', Notes => 'm/s' },
478             0x30 => { Name => 'GPSVelocityNorth', Format => 'float' },
479             0x34 => { Name => 'GPSVelocityUp', Format => 'float' },
480             0x38 => { Name => 'GPSSpeedAccuracy', Format => 'float' },
481             );
482              
483             # tags found in 'camm' type 7 timed metadata (ref 4)
484             %Image::ExifTool::QuickTime::camm7 = (
485             PROCESS_PROC => \&Image::ExifTool::ProcessBinaryData,
486             GROUPS => { 2 => 'Location' },
487             FIRST_ENTRY => 0,
488             4 => {
489             Name => 'MagneticField',
490             Format => 'float[3]',
491             Notes => 'microtesla',
492             },
493             );
494              
495             # preview image stored by TomTom Bandit ActionCam
496             %Image::ExifTool::QuickTime::PreviewInfo = (
497             PROCESS_PROC => \&Image::ExifTool::ProcessBinaryData,
498             FIRST_ENTRY => 0,
499             NOTES => 'Preview stored by TomTom Bandit ActionCam.',
500             8 => {
501             Name => 'PreviewImage',
502             Groups => { 2 => 'Preview' },
503             Binary => 1,
504             Format => 'undef[$size-8]',
505             },
506             );
507              
508             # tags found in 'RVMI' 'gReV' timed metadata (ref PH)
509             %Image::ExifTool::QuickTime::RVMI_gReV = (
510             PROCESS_PROC => \&Image::ExifTool::ProcessBinaryData,
511             GROUPS => { 2 => 'Location' },
512             FIRST_ENTRY => 0,
513             NOTES => 'GPS information extracted from the RVMI box of MOV videos.',
514             4 => {
515             Name => 'GPSLatitude',
516             Format => 'int32s',
517             RawConv => '$$self{FoundGPSLatitude} = 1; $val',
518             ValueConv => 'Image::ExifTool::GPS::ToDegrees($val/1e6, 1)',
519             PrintConv => 'Image::ExifTool::GPS::ToDMS($self, $val, 1, "N")',
520             },
521             8 => {
522             Name => 'GPSLongitude',
523             Format => 'int32s',
524             ValueConv => 'Image::ExifTool::GPS::ToDegrees($val/1e6, 1)',
525             PrintConv => 'Image::ExifTool::GPS::ToDMS($self, $val, 1, "E")',
526             },
527             # 12 - int32s: space for altitude? (always zero in my sample)
528             16 => {
529             Name => 'GPSSpeed', # km/h
530             Format => 'int16s',
531             ValueConv => '$val / 10',
532             },
533             18 => {
534             Name => 'GPSTrack',
535             Format => 'int16u',
536             ValueConv => '$val * 2',
537             },
538             );
539              
540             # tags found in 'RVMI' 'sReV' timed metadata (ref PH)
541             %Image::ExifTool::QuickTime::RVMI_sReV = (
542             PROCESS_PROC => \&Image::ExifTool::ProcessBinaryData,
543             GROUPS => { 2 => 'Location' },
544             FIRST_ENTRY => 0,
545             NOTES => q{
546             G-sensor information extracted from the RVMI box of MOV videos.
547             },
548             4 => {
549             Name => 'GSensor',
550             Format => 'int16s[3]', # X Y Z
551             ValueConv => 'my @a=split " ",$val; $_/=1000 foreach @a; "@a"',
552             },
553             );
554              
555             # tags found in 'tx3g' sbtl timed metadata (ref PH)
556             %Image::ExifTool::QuickTime::tx3g = (
557             PROCESS_PROC => \&Process_tx3g,
558             GROUPS => { 2 => 'Location' },
559             FIRST_ENTRY => 0,
560             NOTES => q{
561             Tags extracted from the tx3g sbtl timed metadata of Yuneec drones, and
562             subtitle text in some other videos.
563             },
564             Lat => {
565             Name => 'GPSLatitude',
566             RawConv => '$$self{FoundGPSLatitude} = 1; $val',
567             PrintConv => 'Image::ExifTool::GPS::ToDMS($self, $val, 1, "N")',
568             },
569             Lon => {
570             Name => 'GPSLongitude',
571             PrintConv => 'Image::ExifTool::GPS::ToDMS($self, $val, 1, "E")',
572             },
573             Alt => {
574             Name => 'GPSAltitude',
575             ValueConv => '$val =~ s/\s*m$//; $val', # remove " m"
576             PrintConv => '"$val m"', # add it back again
577             },
578             Yaw => 'Yaw',
579             Pitch => 'Pitch',
580             Roll => 'Roll',
581             GimYaw => 'GimbalYaw',
582             GimPitch => 'GimbalPitch',
583             GimRoll => 'GimbalRoll',
584             DateTime => { # for date/time-format subtitle text
585             Groups => { 2 => 'Time' },
586             PrintConv => '$self->ConvertDateTime($val)',
587             },
588             Text => { Groups => { 2 => 'Other' } },
589             );
590              
591             %Image::ExifTool::QuickTime::INSV_MakerNotes = (
592             GROUPS => { 1 => 'MakerNotes', 2 => 'Camera' },
593             0x0a => 'SerialNumber',
594             0x12 => 'Model',
595             0x1a => 'Firmware',
596             0x2a => {
597             Name => 'Parameters',
598             ValueConv => '$val =~ tr/_/ /; $val',
599             },
600             );
601              
602             %Image::ExifTool::QuickTime::Tags360Fly = (
603             PROCESS_PROC => \&Process360Fly,
604             NOTES => 'Timed metadata found in MP4 videos from the 360Fly.',
605             1 => {
606             Name => 'Accel360Fly',
607             SubDirectory => { TagTable => 'Image::ExifTool::QuickTime::Accel360Fly' },
608             },
609             2 => {
610             Name => 'Gyro360Fly',
611             SubDirectory => { TagTable => 'Image::ExifTool::QuickTime::Gyro360Fly' },
612             },
613             3 => {
614             Name => 'Mag360Fly',
615             SubDirectory => { TagTable => 'Image::ExifTool::QuickTime::Mag360Fly' },
616             },
617             5 => {
618             Name => 'GPS360Fly',
619             SubDirectory => { TagTable => 'Image::ExifTool::QuickTime::GPS360Fly' },
620             },
621             6 => {
622             Name => 'Rot360Fly',
623             SubDirectory => { TagTable => 'Image::ExifTool::QuickTime::Rot360Fly' },
624             },
625             250 => {
626             Name => 'Fusion360Fly',
627             SubDirectory => { TagTable => 'Image::ExifTool::QuickTime::Fusion360Fly' },
628             },
629             );
630              
631             %Image::ExifTool::QuickTime::Accel360Fly = (
632             PROCESS_PROC => \&Image::ExifTool::ProcessBinaryData,
633             GROUPS => { 2 => 'Location' },
634             1 => { Name => 'AccelMode', Unknown => 1 }, # (always 2 in my sample)
635             2 => {
636             Name => 'SampleTime',
637             Groups => { 2 => 'Video' },
638             Format => 'int64u',
639             ValueConv => '$val / 1e6',
640             PrintConv => 'ConvertDuration($val)',
641             },
642             10 => { Name => 'AccelYPR', Format => 'float[3]' },
643             );
644              
645             %Image::ExifTool::QuickTime::Gyro360Fly = (
646             PROCESS_PROC => \&Image::ExifTool::ProcessBinaryData,
647             GROUPS => { 2 => 'Location' },
648             1 => { Name => 'GyroMode', Unknown => 1 }, # (always 1 in my sample)
649             2 => {
650             Name => 'SampleTime',
651             Groups => { 2 => 'Video' },
652             Format => 'int64u',
653             ValueConv => '$val / 1e6',
654             PrintConv => 'ConvertDuration($val)',
655             },
656             10 => { Name => 'GyroYPR', Format => 'float[3]' },
657             );
658              
659             %Image::ExifTool::QuickTime::Mag360Fly = (
660             PROCESS_PROC => \&Image::ExifTool::ProcessBinaryData,
661             GROUPS => { 2 => 'Location' },
662             1 => { Name => 'MagMode', Unknown => 1 }, # (always 1 in my sample)
663             2 => {
664             Name => 'SampleTime',
665             Groups => { 2 => 'Video' },
666             Format => 'int64u',
667             ValueConv => '$val / 1e6',
668             PrintConv => 'ConvertDuration($val)',
669             },
670             10 => { Name => 'MagnetometerXYZ', Format => 'float[3]' },
671             );
672              
673             %Image::ExifTool::QuickTime::GPS360Fly = (
674             PROCESS_PROC => \&Image::ExifTool::ProcessBinaryData,
675             GROUPS => { 2 => 'Location' },
676             1 => { Name => 'GPSMode', Unknown => 1 }, # (always 16 in my sample)
677             2 => {
678             Name => 'SampleTime',
679             Groups => { 2 => 'Video' },
680             Format => 'int64u',
681             ValueConv => '$val / 1e6',
682             PrintConv => 'ConvertDuration($val)',
683             },
684             10 => { Name => 'GPSLatitude', Format => 'float', PrintConv => 'Image::ExifTool::GPS::ToDMS($self, $val, 1, "N")' },
685             14 => { Name => 'GPSLongitude', Format => 'float', PrintConv => 'Image::ExifTool::GPS::ToDMS($self, $val, 1, "E")' },
686             18 => { Name => 'GPSAltitude', Format => 'float', PrintConv => '"$val m"' }, # (questionable accuracy)
687             22 => {
688             Name => 'GPSSpeed',
689             Notes => 'converted to km/hr',
690             Format => 'int16u',
691             ValueConv => '$val * 0.036',
692             PrintConv => 'sprintf("%.1f",$val)',
693             },
694             24 => { Name => 'GPSTrack', Format => 'int16u', ValueConv => '$val / 100' },
695             26 => { Name => 'Acceleration', Format => 'int16u', ValueConv => '$val / 1000' },
696             );
697              
698             %Image::ExifTool::QuickTime::Rot360Fly = (
699             PROCESS_PROC => \&Image::ExifTool::ProcessBinaryData,
700             GROUPS => { 2 => 'Location' },
701             1 => { Name => 'RotMode', Unknown => 1 }, # (always 1 in my sample)
702             2 => {
703             Name => 'SampleTime',
704             Groups => { 2 => 'Video' },
705             Format => 'int64u',
706             ValueConv => '$val / 1e6',
707             PrintConv => 'ConvertDuration($val)',
708             },
709             10 => { Name => 'RotationXYZ', Format => 'float[3]' },
710             );
711              
712             %Image::ExifTool::QuickTime::Fusion360Fly = (
713             PROCESS_PROC => \&Image::ExifTool::ProcessBinaryData,
714             GROUPS => { 2 => 'Location' },
715             1 => { Name => 'FusionMode', Unknown => 1 }, # (always 0 in my sample)
716             2 => {
717             Name => 'SampleTime',
718             Groups => { 2 => 'Video' },
719             Format => 'int64u',
720             ValueConv => '$val / 1e6',
721             PrintConv => 'ConvertDuration($val)',
722             },
723             10 => { Name => 'FusionYPR', Format => 'float[3]' },
724             );
725              
726             # tags found in 'marl' ctbx timed metadata (ref PH)
727             %Image::ExifTool::QuickTime::marl = (
728             PROCESS_PROC => \&Process_marl,
729             GROUPS => { 2 => 'Other' },
730             NOTES => 'Tags extracted from the marl ctbx timed metadata of GM cars.',
731             );
732              
733             #------------------------------------------------------------------------------
734             # Save information from keys in OtherSampleDesc directory for processing timed metadata
735             # Inputs: 0) ExifTool object ref, 1) dirInfo ref, 2) tag table ref
736             # Returns: 1 on success
737             # (ref "Timed Metadata Media" here:
738             # https://developer.apple.com/library/content/documentation/QuickTime/QTFF/QTFFChap3/qtff3.html)
739             sub SaveMetaKeys($$$)
740             {
741 0     0 0 0 local $_;
742 0         0 my ($et, $dirInfo, $tagTbl) = @_;
743 0         0 my $dataPt = $$dirInfo{DataPt};
744 0         0 my $dirLen = length $$dataPt;
745 0 0       0 return 0 unless $dirLen > 8;
746 0         0 my $pos = 0;
747 0         0 my $verbose = $$et{OPTIONS}{Verbose};
748 0         0 my $oldIndent = $$et{INDENT};
749 0         0 my $ee = $$et{ee};
750 0 0       0 $ee or $ee = $$et{ee} = { };
751              
752 0 0       0 $verbose and $et->VerboseDir($$dirInfo{DirName}, undef, $dirLen);
753              
754             # loop through metadata key table
755 0         0 while ($pos + 8 < $dirLen) {
756 0         0 my $size = Get32u($dataPt, $pos);
757 0         0 my $id = substr($$dataPt, $pos+4, 4);
758 0         0 my $end = $pos + $size;
759 0 0       0 $end = $dirLen if $end > $dirLen;
760 0         0 $pos += 8;
761 0         0 my ($tagID, $format, $pid);
762 0 0       0 if ($verbose) {
763 0         0 $pid = PrintableTagID($id,1);
764 0         0 $et->VPrint(0, "$oldIndent+ [Metadata Key entry, Local ID=$pid, $size bytes]\n");
765 0         0 $$et{INDENT} .= '| ';
766             }
767              
768 0         0 while ($pos + 4 < $end) {
769 0         0 my $len = unpack("x${pos}N", $$dataPt);
770 0 0 0     0 last if $len < 8 or $pos + $len > $end;
771 0         0 my $tag = substr($$dataPt, $pos + 4, 4);
772 0         0 $pos += 8; $len -= 8;
  0         0  
773 0         0 my $val = substr($$dataPt, $pos, $len);
774 0         0 $pos += $len;
775 0         0 my $str;
776 0 0       0 if ($tag eq 'keyd') {
    0          
777 0         0 ($tagID = $val) =~ s/^(mdta|fiel)com\.apple\.quicktime\.//;
778 0 0       0 $tagID = "Tag_$val" unless $tagID;
779 0 0       0 ($str = $val) =~ s/(.{4})/$1 / if $verbose;
780             } elsif ($tag eq 'dtyp') {
781 0 0       0 next if length $val < 4;
782 0 0       0 if (length $val >= 4) {
783 0         0 my $ns = unpack('N', $val);
784 0 0       0 if ($ns == 0) {
    0          
785 0 0       0 length $val >= 8 or $et->Warn('Short dtyp data'), next;
786 0         0 $str = unpack('x4N',$val);
787 0   0     0 $format = $qtFmt{$str} || 'undef';
788             } elsif ($ns == 1) {
789 0         0 $str = substr($val, 4);
790 0         0 $format = 'undef';
791             } else {
792 0         0 $format = 'undef';
793             }
794 0 0 0     0 $str .= " ($format)" if $verbose and defined $str;
795             }
796             }
797 0 0       0 if ($verbose > 1) {
798 0 0       0 if (defined $str) {
799 0         0 $str =~ tr/\x00-\x1f\x7f-\xff/./;
800 0         0 $str = " = $str";
801             } else {
802 0         0 $str = '';
803             }
804 0         0 $et->VPrint(1, $$et{INDENT}."- Tag '".PrintableTagID($tag,2)."' ($len bytes)$str\n");
805 0         0 $et->VerboseDump(\$val);
806             }
807             }
808 0 0 0     0 if (defined $tagID and defined $format) {
809 0 0       0 if ($verbose) {
810 0         0 my $t2 = PrintableTagID($tagID);
811 0         0 $et->VPrint(0, "$$et{INDENT}Added Local ID $pid = $t2 ($format)\n");
812             }
813 0         0 $$ee{'keys'}{$id} = { TagID => $tagID, Format => $format };
814             }
815 0         0 $$et{INDENT} = $oldIndent;
816             }
817 0         0 return 1;
818             }
819              
820             #------------------------------------------------------------------------------
821             # We found some tags for this sample, so set document number and save timing information
822             # Inputs: 0) ExifTool ref, 1) tag table ref, 2) sample time, 3) sample duration
823             sub FoundSomething($$;$$)
824             {
825 8     8 0 29 my ($et, $tagTbl, $time, $dur) = @_;
826 8         28 $$et{DOC_NUM} = ++$$et{DOC_COUNT};
827 8 50       51 $et->HandleTag($tagTbl, SampleTime => $time) if defined $time;
828 8 50       49 $et->HandleTag($tagTbl, SampleDuration => $dur) if defined $dur;
829             }
830              
831             #------------------------------------------------------------------------------
832             # Approximate GPSDateTime value from sample time and CreateDate
833             # Inputs: 0) ExifTool ref, 1) tag table ptr, 2) sample time (s)
834             # 3) true if CreateDate is at end of video
835             # Notes: Uses ExifTool CreateDateAtEnd as flag to subtract video duration
836             sub SetGPSDateTime($$$)
837             {
838 0     0 0 0 my ($et, $tagTbl, $sampleTime) = @_;
839 0         0 my $value = $$et{VALUE};
840 0 0 0     0 if (defined $sampleTime and $$value{CreateDate}) {
841 0         0 $sampleTime += $$value{CreateDate}; # adjust sample time to seconds since the epoch
842 0 0       0 if ($$et{CreateDateAtEnd}) { # adjust if CreateDate is at end of video
843 0 0 0     0 return unless $$value{TimeScale} and $$value{Duration};
844 0         0 $sampleTime -= $$value{Duration} / $$value{TimeScale};
845 0         0 $et->WarnOnce('Approximating GPSDateTime as CreateDate - Duration + SampleTime', 1);
846             } else {
847 0         0 $et->WarnOnce('Approximating GPSDateTime as CreateDate + SampleTime', 1);
848             }
849 0 0       0 unless ($et->Options('QuickTimeUTC')) {
850 0         0 my $tzOff = $$et{tzOff}; # use previously calculated offset
851 0 0       0 unless (defined $tzOff) {
852             # adjust to UTC, assuming time is local
853 0         0 my @tm = localtime $$value{CreateDate};
854 0         0 my @gm = gmtime $$value{CreateDate};
855 0         0 $tzOff = $$et{tzOff} = Image::ExifTool::GetTimeZone(\@tm, \@gm) * 60;
856             }
857 0         0 $sampleTime -= $tzOff; # shift from local time to UTC
858             }
859 0         0 $et->HandleTag($tagTbl, GPSDateTime => Image::ExifTool::ConvertUnixTime($sampleTime,0,3) . 'Z');
860             }
861             }
862              
863             #------------------------------------------------------------------------------
864             # Handle tags that we found in the subtitle 'text'
865             # Inputs: 0) ExifTool ref, 1) tag table ref, 2) hash of tag names/values
866             sub HandleTextTags($$$)
867             {
868 0     0 0 0 my ($et, $tagTbl, $tags) = @_;
869 0         0 my $tag;
870 0         0 delete $$tags{done};
871 0 0       0 delete $$tags{GPSTimeStamp} if $$tags{GPSDateTime};
872 0         0 foreach $tag (sort keys %$tags) {
873 0         0 $et->HandleTag($tagTbl, $tag => $$tags{$tag});
874             }
875 0         0 $$et{UnknownTextCount} = 0;
876 0         0 undef %$tags; # clear the hash
877             }
878              
879             #------------------------------------------------------------------------------
880             # Process subtitle 'text'
881             # Inputs: 0) ExifTool ref, 1) data ref or dirInfo ref, 2) tag table ref
882             sub Process_text($$$)
883             {
884 0     0 0 0 my ($et, $dataPt, $tagTbl) = @_;
885 0         0 my %tags;
886              
887 0 0       0 return if $$et{NoMoreTextDecoding};
888              
889 0 0       0 if (ref $dataPt eq 'HASH') {
890 0         0 my $dirName = $$dataPt{DirName};
891 0         0 $dataPt = $$dataPt{DataPt};
892 0         0 $et->VerboseDir($dirName, undef, length($$dataPt));
893             }
894              
895 0         0 while ($$dataPt =~ /\$(\w+)([^\$]*)/g) {
896 0         0 my ($tag, $dat) = ($1, $2);
897 0 0 0     0 if ($tag =~ /^[A-Z]{2}RMC$/ and $dat =~ /^,(\d{2})(\d{2})(\d+(?:\.\d*)),A?,(\d*?)(\d{1,2}\.\d+),([NS]),(\d*?)(\d{1,2}\.\d+),([EW]),(\d*\.?\d*),(\d*\.?\d*),(\d{2})(\d{2})(\d+)/) {
    0 0        
    0 0        
    0 0        
    0          
    0          
898 0         0 my $time = "$1:$2:$3";
899 0 0       0 if ($$et{LastTime}) {
900 0 0       0 if ($$et{LastTime} eq $time) {
    0          
901             # combine with the previous NMEA sentence
902 0         0 $$et{DOC_NUM} = $$et{LastDoc};
903             } elsif (%tags) {
904             # handle existing tags and start a new document
905             # (see https://exiftool.org/forum/index.php?msg=75422)
906 0         0 HandleTextTags($et, $tagTbl, \%tags);
907 0         0 undef %tags;
908             # increment document number and update document count if necessary
909 0 0       0 $$et{DOC_COUNT} < ++$$et{DOC_NUM} and $$et{DOC_COUNT} = $$et{DOC_NUM};
910             }
911             }
912 0         0 $$et{LastTime} = $time;
913 0         0 $$et{LastDoc} = $$et{DOC_NUM};
914 0 0       0 my $year = $14 + ($14 >= 70 ? 1900 : 2000);
915 0         0 my $dateTime = sprintf('%.4d:%.2d:%.2d %sZ', $year, $13, $12, $time);
916 0         0 $tags{GPSDateTime} = $dateTime;
917 0 0 0     0 $tags{GPSLatitude} = (($4 || 0) + $5/60) * ($6 eq 'N' ? 1 : -1);
918 0 0 0     0 $tags{GPSLongitude} = (($7 || 0) + $8/60) * ($9 eq 'E' ? 1 : -1);
919 0 0       0 $tags{GPSSpeed} = $10 * $knotsToKph if length $10;
920 0 0       0 $tags{GPSTrack} = $11 if length $11;
921             } elsif ($tag =~ /^[A-Z]{2}GGA$/ and $dat =~ /^,(\d{2})(\d{2})(\d+(?:\.\d*)?),(\d*?)(\d{1,2}\.\d+),([NS]),(\d*?)(\d{1,2}\.\d+),([EW]),[1-6]?,(\d+)?,(\.\d+|\d+\.?\d*)?,(-?\d+\.?\d*)?,M?/s) {
922 0         0 my $time = "$1:$2:$3";
923 0 0       0 if ($$et{LastTime}) {
924 0 0       0 if ($$et{LastTime} eq $time) {
    0          
925 0         0 $$et{DOC_NUM} = $$et{LastDoc};
926             } elsif (%tags) {
927 0         0 HandleTextTags($et, $tagTbl, \%tags);
928 0         0 undef %tags;
929 0 0       0 $$et{DOC_COUNT} < ++$$et{DOC_NUM} and $$et{DOC_COUNT} = $$et{DOC_NUM};
930             }
931             }
932 0         0 $$et{LastTime} = $time;
933 0         0 $$et{LastDoc} = $$et{DOC_NUM};
934 0         0 $tags{GPSTimeStamp} = $time;
935 0 0 0     0 $tags{GPSLatitude} = (($4 || 0) + $5/60) * ($6 eq 'N' ? 1 : -1);
936 0 0 0     0 $tags{GPSLongitude} = (($7 || 0) + $8/60) * ($9 eq 'E' ? 1 : -1);
937 0 0       0 $tags{GPSSatellites} = $10 if defined $10;
938 0 0       0 $tags{GPSDOP} = $11 if defined $11;
939 0 0       0 $tags{GPSAltitude} = $12 if defined $12;
940             } elsif ($tag eq 'BEGINGSENSOR' and $dat =~ /^:([-+]\d+\.\d+):([-+]\d+\.\d+):([-+]\d+\.\d+)/) {
941 0         0 $tags{Accelerometer} = "$1 $2 $3";
942             } elsif ($tag eq 'TIME' and $dat =~ /^:(\d+)/) {
943 0   0     0 $tags{TimeCode} = $1 / ($$et{MediaTS} || 1);
944             } elsif ($tag eq 'BEGIN') {
945 0 0       0 $tags{Text} = $dat if length $dat;
946 0         0 $tags{done} = 1;
947             } elsif ($tag ne 'END') {
948 0         0 $tags{Text} = "\$$tag$dat";
949             }
950             }
951 0 0       0 %tags and HandleTextTags($et, $tagTbl, \%tags), return;
952              
953             # check for enciphered binary GPS data
954             # BlueSkySea:
955             # 0000: 00 00 aa aa aa aa 54 54 98 9a 9b 93 9a 92 98 9a [......TT........]
956             # 0010: 9a 9d 9f 9b 9f 9d aa aa aa aa aa aa aa aa aa aa [................]
957             # 0020: aa aa aa aa aa a9 e4 9e 92 9f 9b 9f 92 9d 99 ef [................]
958             # 0030: 9a 9a 98 9b 93 9d 9d 9c 93 aa aa aa aa aa 9a 99 [................]
959             # 0040: 9b aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa [................]
960             # [...]
961             # decrypted:
962             # 0000: aa aa 00 00 00 00 fe fe 32 30 31 39 30 38 32 30 [........20190820]
963             # 0010: 30 37 35 31 35 37 00 00 00 00 00 00 00 00 00 00 [075157..........]
964             # 0020: 00 00 00 00 00 03 4e 34 38 35 31 35 38 37 33 45 [......N48515873E]
965             # 0030: 30 30 32 31 39 37 37 36 39 00 00 00 00 00 30 33 [002197769.....03]
966             # 0040: 31 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 [1...............]
967             # [...]
968             # Ambarella A12:
969             # 0000: 00 00 f2 e1 f0 ee 54 54 98 9a 9b 93 9b 9b 9b 9c [......TT........]
970             # 0010: 9b 9a 9a 93 9a 9b a6 9a 9b 9b 93 9b 9a 9b 9c 9a [................]
971             # 0020: 9d 9a 92 9f 93 a9 e4 9f 9f 9e 9f 9b 9b 9c 9d ef [................]
972             # 0030: 9a 99 9d 9e 99 9a 9a 9e 9b 81 9a 9b 9f 9d 9a 9a [................]
973             # 0040: 9a 87 9a 9a 9a 87 9a 98 99 87 9a 9a 99 87 9a 9a [................]
974             # [...]
975             # decrypted:
976             # 0000: aa aa 58 4b 5a 44 fe fe 32 30 31 39 31 31 31 36 [..XKZD..20191116]
977             # 0010: 31 30 30 39 30 31 0c 30 31 31 39 31 30 31 36 30 [100901.011910160]
978             # 0020: 37 30 38 35 39 03 4e 35 35 34 35 31 31 36 37 45 [70859.N55451167E]
979             # 0030: 30 33 37 34 33 30 30 34 31 2b 30 31 35 37 30 30 [037430041+015700]
980             # 0040: 30 2d 30 30 30 2d 30 32 33 2d 30 30 33 2d 30 30 [0-000-023-003-00]
981             # [...]
982             # 0100: aa 55 57 ed ed 45 58 54 44 00 01 30 30 30 30 31 [.UW..EXTD..00001]
983             # 0110: 31 30 38 30 30 30 58 00 58 00 58 00 58 00 58 00 [108000X.X.X.X.X.]
984             # 0120: 58 00 58 00 58 00 58 00 00 00 00 00 00 00 00 00 [X.X.X.X.........]
985             # 0130: 00 00 00 00 00 00 00 [.......]
986 0 0 0     0 if ($$dataPt =~ /^\0\0(..\xaa\xaa|\xf2\xe1\xf0\xee)/s and length $$dataPt >= 282) {
987 0         0 my $val = pack('C*', map { $_ ^ 0xaa } unpack('C*', substr($$dataPt, 8, 14)));
  0         0  
988 0 0       0 if ($val =~ /^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})$/) {
989 0         0 $tags{GPSDateTime} = "$1:$2:$3 $4:$5:$6";
990 0         0 $val = pack('C*', map { $_ ^ 0xaa } unpack('C*', substr($$dataPt, 38, 9)));
  0         0  
991 0 0       0 if ($val =~ /^([NS])(\d{2})(\d+$)$/) {
992 0 0       0 $tags{GPSLatitude} = ($2 + $3 / 600000) * ($1 eq 'S' ? -1 : 1);
993             }
994 0         0 $val = pack('C*', map { $_ ^ 0xaa } unpack('C*', substr($$dataPt, 47, 10)));
  0         0  
995 0 0       0 if ($val =~ /^([EW])(\d{3})(\d+$)$/) {
996 0 0       0 $tags{GPSLongitude} = ($2 + $3 / 600000) * ($1 eq 'W' ? -1 : 1);
997             }
998 0         0 $val = pack('C*', map { $_ ^ 0xaa } unpack('C*', substr($$dataPt, 0x39, 5)));
  0         0  
999 0 0       0 $tags{GPSAltitude} = $val + 0 if $val =~ /^[-+]\d+$/;
1000 0         0 $val = pack('C*', map { $_ ^ 0xaa } unpack('C*', substr($$dataPt, 0x3e, 3)));
  0         0  
1001 0 0       0 $tags{GPSSpeed} = $val + 0 if $val =~ /^\d+$/;
1002 0 0       0 if ($$dataPt =~ /^\0\0..\xaa\xaa/s) { # (BlueSkySea)
1003 0         0 $val = pack('C*', map { $_ ^ 0xaa } unpack('C*', substr($$dataPt, 0xad, 12)));
  0         0  
1004             # the first X,Y,Z accelerometer readings from the AccelerometerData
1005 0 0       0 if ($val =~ /^([-+]\d{3})([-+]\d{3})([-+]\d{3})$/) {
1006 0         0 $tags{Accelerometer} = "$1 $2 $3";
1007 0         0 $val = pack('C*', map { $_ ^ 0xaa } unpack('C*', substr($$dataPt, 0xba, 96)));
  0         0  
1008 0         0 my $order = GetByteOrder();
1009 0         0 SetByteOrder('II');
1010 0         0 $val = ReadValue(\$val, 0, 'float');
1011 0         0 SetByteOrder($order);
1012 0         0 $tags{AccelerometerData} = $val;
1013             }
1014             } else { # (Ambarella)
1015 0         0 my @acc;
1016 0         0 $val = pack('C*', map { $_ ^ 0xaa } unpack('C*', substr($$dataPt, 0x41, 195)));
  0         0  
1017 0         0 push @acc, $1, $2, $3 while $val =~ /\G([-+]\d{3})([-+]\d{3})([-+]\d{3})/g;
1018 0 0       0 $tags{Accelerometer} = "@acc" if @acc;
1019             }
1020             }
1021 0 0       0 %tags and HandleTextTags($et, $tagTbl, \%tags), return;
1022             }
1023              
1024             # check for DJI telemetry data, eg:
1025             # "F/3.5, SS 1000, ISO 100, EV 0, GPS (8.6499, 53.1665, 18), D 24.26m,
1026             # H 6.00m, H.S 2.10m/s, V.S 0.00m/s \n"
1027 0 0       0 if ($$dataPt =~ /GPS \(([-+]?\d*\.\d+),\s*([-+]?\d*\.\d+)/) {
1028 0         0 $$et{CreateDateAtEnd} = 1; # set flag indicating the file creation date is at the end
1029 0         0 $tags{GPSLatitude} = $2;
1030 0         0 $tags{GPSLongitude} = $1;
1031 0 0       0 $tags{GPSAltitude} = $1 if $$dataPt =~ /,\s*H\s+([-+]?\d+\.?\d*)m/;
1032 0 0       0 $tags{GPSSpeed} = $1 * $mpsToKph if $$dataPt =~ /,\s*H.S\s+([-+]?\d+\.?\d*)/;
1033 0 0       0 $tags{Distance} = $1 * $mpsToKph if $$dataPt =~ /,\s*D\s+(\d+\.?\d*)m/;
1034 0 0       0 $tags{VerticalSpeed} = $1 if $$dataPt =~ /,\s*V.S\s+([-+]?\d+\.?\d*)/;
1035 0 0       0 $tags{FNumber} = $1 if $$dataPt =~ /\bF\/(\d+\.?\d*)/;
1036 0 0       0 $tags{ExposureTime} = 1 / $1 if $$dataPt =~ /\bSS\s+(\d+\.?\d*)/;
1037 0 0 0     0 $tags{ExposureCompensation} = ($1 / ($2 || 1)) if $$dataPt =~ /\bEV\s+([-+]?\d+\.?\d*)(\/\d+)?/;
1038 0 0       0 $tags{ISO} = $1 if $$dataPt =~ /\bISO\s+(\d+\.?\d*)/;
1039 0         0 HandleTextTags($et, $tagTbl, \%tags);
1040 0         0 return;
1041             }
1042              
1043             # check for Mini 0806 dashcam GPS, eg:
1044             # "A,270519,201555.000,3356.8925,N,08420.2071,W,000.0,331.0M,+01.84,-09.80,-00.61;\n"
1045 0 0       0 if ($$dataPt =~ /^A,(\d{2})(\d{2})(\d{2}),(\d{2})(\d{2})(\d{2}(\.\d+)?)/) {
1046 0         0 $tags{GPSDateTime} = "20$3:$2:$1 $4:$5:$6Z";
1047 0 0       0 if ($$dataPt =~ /^A,.*?,.*?,(\d{2})(\d+\.\d+),([NS])/) {
1048 0 0       0 $tags{GPSLatitude} = ($1 + $2/60) * ($3 eq 'S' ? -1 : 1);
1049             }
1050 0 0       0 if ($$dataPt =~ /^A,.*?,.*?,.*?,.*?,(\d{3})(\d+\.\d+),([EW])/) {
1051 0 0       0 $tags{GPSLongitude} = ($1 + $2/60) * ($3 eq 'W' ? -1 : 1);
1052             }
1053 0         0 my @a = split ',', $$dataPt;
1054 0 0 0     0 $tags{GPSAltitude} = $a[8] if $a[8] and $a[8] =~ s/M$//;
1055 0 0 0     0 $tags{GPSSpeed} = $a[7] if $a[7] and $a[7] =~ /^\d+\.\d+$/; # (NC)
1056 0 0 0     0 $tags{Accelerometer} = "$a[9] $a[10] $a[11]" if $a[11] and $a[11] =~ s/;\s*$//;
1057 0         0 HandleTextTags($et, $tagTbl, \%tags);
1058 0         0 return;
1059             }
1060              
1061             # check for Roadhawk dashcam text
1062             # ".;;;;D?JL;6+;;;D;R?;4;;;;DBB;;O;;;=D;L;;HO71G>F;-?=J-F:FNJJ;DPP-JF3F;;PL=DBRLBF0F;=?DNF-RD-PF;N;?=JF;;?D=F:*6F~"
1063             # decoded:
1064             # "X0000.2340Y-000.0720Z0000.9900G0001.0400$GPRMC,082138,A,5330.6683,N,00641.9749,W,012.5,87.86,050213,002.1,A"
1065             # (note: "002.1" is magnetic variation and is not decoded; it should have ",E" or ",W" afterward for direction)
1066 0 0       0 if ($$dataPt =~ /\*[0-9A-F]{2}~$/) {
1067             # (ref https://reverseengineering.stackexchange.com/questions/11582/how-to-reverse-engineer-dash-cam-metadata)
1068 0         0 my @decode = unpack 'C*', '-I8XQWRVNZOYPUTA0B1C2SJ9K.L,M$D3E4F5G6H7';
1069 0         0 my @chars = unpack 'C*', substr($$dataPt, 0, -4);
1070 0         0 foreach (@chars) {
1071 0         0 my $n = $_ - 43;
1072 0 0 0     0 $_ = $decode[$n] if $n >= 0 and defined $decode[$n];
1073             }
1074 0         0 my $buff = pack 'C*', @chars;
1075 0 0       0 if ($buff =~ /X(.*?)Y(.*?)Z(.*?)G(.*?)\$/) {
1076             # yup. the decoding worked out
1077 0         0 $tags{Accelerometer} = "$1 $2 $3 $4";
1078 0         0 $$dataPt = $buff; # (process GPRMC below)
1079             }
1080             }
1081              
1082             # check for Thinkware format (and other NMEA RMC), eg:
1083             # "gsensori,4,512,-67,-12,100;GNRMC,161313.00,A,4529.87489,N,07337.01215,W,6.225,35.34,310819,,,A*52..;
1084             # CAR,0,0,0,0.0,0,0,0,0,0,0,0,0"
1085 0 0 0     0 if ($$dataPt =~ /[A-Z]{2}RMC,(\d{2})(\d{2})(\d+(\.\d*)?),A?,(\d*?)(\d{1,2}\.\d+),([NS]),(\d*?)(\d{1,2}\.\d+),([EW]),(\d*\.?\d*),(\d*\.?\d*),(\d{2})(\d{2})(\d+)/ and
      0        
      0        
1086             # do some basic sanity checks on the date
1087             $13 <= 31 and $14 <= 12 and $15 <= 99)
1088             {
1089 0 0       0 my $year = $15 + ($15 >= 70 ? 1900 : 2000);
1090 0         0 $tags{GPSDateTime} = sprintf('%.4d:%.2d:%.2d %.2d:%.2d:%.2dZ', $year, $14, $13, $1, $2, $3);
1091 0 0 0     0 $tags{GPSLatitude} = (($5 || 0) + $6/60) * ($7 eq 'N' ? 1 : -1);
1092 0 0 0     0 $tags{GPSLongitude} = (($8 || 0) + $9/60) * ($10 eq 'E' ? 1 : -1);
1093 0 0       0 $tags{GPSSpeed} = $11 * $knotsToKph if length $11;
1094 0 0       0 $tags{GPSTrack} = $12 if length $12;
1095             }
1096 0 0       0 $tags{GSensor} = $1 if $$dataPt =~ /\bgsensori,(.*?)(;|$)/;
1097 0 0       0 $tags{Car} = $1 if $$dataPt =~ /\bCAR,(.*?)(;|$)/;
1098              
1099 0 0       0 if (%tags) {
1100 0         0 HandleTextTags($et, $tagTbl, \%tags);
1101             } else {
1102 0   0     0 $$et{UnknownTextCount} = ($$et{UnknownTextCount} || 0) + 1;
1103             # give up trying to decode useful information if we haven't found anything for a while
1104 0 0       0 $$et{NoMoreTextDecoding} = 1 if $$et{UnknownTextCount} > 100;
1105             }
1106             }
1107              
1108             #------------------------------------------------------------------------------
1109             # Extract embedded metadata from media samples
1110             # Inputs: 0) ExifTool ref
1111             # Notes: Also accesses ExifTool RAF*, SET_GROUP1, HandlerType, MetaFormat,
1112             # ee*, and avcC elements (* = must exist)
1113             sub ProcessSamples($)
1114             {
1115 16     16 0 36 my $et = shift;
1116 16         55 my ($raf, $ee) = @$et{qw(RAF ee)};
1117 16         44 my ($i, $buff, $pos, $hdrLen, $hdrFmt, @time, @dur, $oldIndent);
1118              
1119 16 50       47 return unless $ee;
1120 16         40 delete $$et{ee}; # use only once
1121              
1122             # only process specific types of video streams
1123 16   50     691 my $type = $$et{HandlerType} || '';
1124 16 100       48 if ($type eq 'vide') {
1125 12 50       54 if ($$ee{avcC}) { $type = 'avcC' }
  0 100       0  
1126 4         13 elsif ($$ee{JPEG}) { $type = 'JPEG' }
1127 8         41 else { return }
1128             }
1129              
1130 8         33 my ($start, $size) = @$ee{qw(start size)};
1131             #
1132             # determine sample start offsets from chunk offsets (stco) and sample-to-chunk table (stsc),
1133             # and sample time/duration from time-to-sample (stts)
1134             #
1135 8 50 33     32 unless ($start and $size) {
1136 8 50       27 return unless $size;
1137 8         28 my ($stco, $stsc, $stts) = @$ee{qw(stco stsc stts)};
1138 8 50 33     82 return unless $stco and $stsc and @$stsc;
      33        
1139 8         21 $start = [ ];
1140 8         28 my ($nextChunk, $iChunk) = (0, 1);
1141 8         22 my ($chunkStart, $startChunk, $samplesPerChunk, $descIdx, $timeCount, $timeDelta, $time);
1142 8 50 33     49 if ($stts and @$stts > 1) {
1143 8         16 $time = 0;
1144 8         26 $timeCount = shift @$stts;
1145 8         19 $timeDelta = shift @$stts;
1146             }
1147 8   50     32 my $ts = $$et{MediaTS} || 1;
1148 8         35 foreach $chunkStart (@$stco) {
1149 8 50 33     49 if ($iChunk >= $nextChunk and @$stsc) {
1150 8         27 ($startChunk, $samplesPerChunk, $descIdx) = @{shift @$stsc};
  8         27  
1151 8 50       33 $nextChunk = $$stsc[0][0] if @$stsc;
1152             }
1153 8 50       30 @$size < @$start + $samplesPerChunk and $et->WarnOnce('Sample size error'), last;
1154 8         19 my $sampleStart = $chunkStart;
1155 8         19 Sample: for ($i=0; ; ) {
1156 8         22 push @$start, $sampleStart;
1157 8 50       36 if (defined $time) {
1158 8         30 until ($timeCount) {
1159 0 0       0 if (@$stts < 2) {
1160 0         0 undef $time;
1161 0         0 last Sample;
1162             }
1163 0         0 $timeCount = shift @$stts;
1164 0         0 $timeDelta = shift @$stts;
1165             }
1166 8         26 push @time, $time / $ts;
1167 8         23 push @dur, $timeDelta / $ts;
1168 8         20 $time += $timeDelta;
1169 8         15 --$timeCount;
1170             }
1171             # (eventually should use the description indices: $descIdx)
1172 8 50       33 last if ++$i >= $samplesPerChunk;
1173 0         0 $sampleStart += $$size[$#$start];
1174             }
1175 8         22 ++$iChunk;
1176             }
1177 8 50       40 @$start == @$size or $et->WarnOnce('Incorrect sample start/size count'), return;
1178             }
1179             #
1180             # extract and parse the sample data
1181             #
1182 8         36 my $tagTbl = GetTagTable('Image::ExifTool::QuickTime::Stream');
1183 8         42 my $verbose = $et->Options('Verbose');
1184 8   100     50 my $metaFormat = $$et{MetaFormat} || '';
1185 8         40 my $tell = $raf->Tell();
1186              
1187 8 50       29 if ($verbose) {
1188 0         0 $et->VPrint(0, "---- Extract Embedded ----\n");
1189 0         0 $oldIndent = $$et{INDENT};
1190 0         0 $$et{INDENT} = '';
1191             }
1192             # get required information from avcC box if parsing video data
1193 8 50       36 if ($type eq 'avcC') {
1194 0         0 $hdrLen = (Get8u(\$$ee{avcC}, 4) & 0x03) + 1;
1195 0 0       0 $hdrFmt = ($hdrLen == 4 ? 'N' : $hdrLen == 2 ? 'n' : 'C');
    0          
1196 0         0 require Image::ExifTool::H264;
1197             }
1198             # loop through all samples
1199 8   66     62 for ($i=0; $i<@$start and $i<@$size; ++$i) {
1200              
1201             # initialize our flags for setting GPSDateTime
1202 8         19 delete $$et{FoundGPSLatitude};
1203 8         19 delete $$et{FoundGPSDateTime};
1204              
1205             # read the sample data
1206 8         23 my $size = $$size[$i];
1207 8 50 33     40 next unless $raf->Seek($$start[$i], 0) and $raf->Read($buff, $size) == $size;
1208              
1209 8 50       48 if ($type eq 'avcC') {
1210 0 0       0 next if length($buff) <= $hdrLen;
1211             # scan through all NAL units and send them to ParseH264Video()
1212 0         0 for ($pos=0; ; ) {
1213 0         0 my $len = unpack("x$pos$hdrFmt", $buff);
1214 0 0       0 last if $pos + $hdrLen + $len > length($buff);
1215 0         0 my $tmp = "\0\0\0\x01" . substr($buff, $pos+$hdrLen, $len);
1216 0         0 Image::ExifTool::H264::ParseH264Video($et, \$tmp);
1217 0         0 $pos += $hdrLen + $len;
1218 0 0       0 last if $pos + $hdrLen >= length($buff);
1219             }
1220 0 0       0 if ($$et{GotNAL06}) {
1221 0         0 my $eeOpt = $et->Options('ExtractEmbedded');
1222 0 0 0     0 last unless $eeOpt and $eeOpt > 2;
1223             }
1224 0         0 next;
1225             }
1226 8 50       41 if ($verbose > 1) {
1227 0 0       0 my $hdr = $$et{SET_GROUP1} ? "$$et{SET_GROUP1} Type='${type}' Format='${metaFormat}'" : "Type='${type}'";
1228 0         0 $et->VPrint(1, "${hdr}, Sample ".($i+1).' of '.scalar(@$start)." ($size bytes)\n");
1229 0         0 $et->VerboseDump(\$buff, Addr => $$start[$i]);
1230             }
1231 8 50 33     122 if ($type eq 'text' or
    100 33        
    50 33        
    50          
1232             # (PNDM is normally 'text', but was sbtl/tx3g in concatenated Garmin sample output_3videos.mp4)
1233             ($type eq 'sbtl' and $metaFormat eq 'tx3g' and $buff =~ /^..PNDM/s))
1234             {
1235              
1236 0         0 FoundSomething($et, $tagTbl, $time[$i], $dur[$i]);
1237 0 0       0 unless ($buff =~ /^\$BEGIN/) {
1238             # remove ending "encd" box if it exists
1239 0 0       0 $buff =~ s/\0\0\0\x0cencd\0\0\x01\0$// and $size -= 12;
1240             # cameras such as the CanonPowerShotN100 store ASCII time codes with a
1241             # leading 2-byte integer giving the length of the string
1242             # (and chapter names start with a 2-byte integer too)
1243 0 0 0     0 if ($size >= 2 and unpack('n',$buff) == $size - 2) {
1244 0 0       0 next if $size == 2;
1245 0         0 $buff = substr($buff,2);
1246             }
1247 0         0 my $val;
1248             # check for encrypted GPS text as written by E-PRANCE B47FS camera
1249 0 0 0     0 if ($buff =~ /^\0/ and $buff =~ /\x0a$/ and length($buff) > 5) {
    0 0        
1250             # decode simple ASCII difference cipher,
1251             # based on known value of 4th-last char = '*'
1252 0         0 my $dif = ord('*') - ord(substr($buff, -4, 1));
1253 0         0 my $tmp = pack 'C*',map { $_=($_+$dif)&0xff } unpack 'C*',substr $buff,1,-1;
  0         0  
1254 0 0       0 if ($verbose > 2) {
1255 0         0 $et->VPrint(0, "[decrypted text]\n");
1256 0         0 $et->VerboseDump(\$tmp);
1257             }
1258 0 0       0 if ($tmp =~ /^(.*?)(\$[A-Z]{2}RMC.*)/s) {
1259 0         0 ($val, $buff) = ($1, $2);
1260 0         0 $val =~ tr/\t/ /;
1261 0 0       0 $et->HandleTag($tagTbl, RawGSensor => $val) if length $val;
1262             }
1263             } elsif ($buff =~ /^(\0.{3})?PNDM/s) {
1264             # Garmin Dashcam format (actually binary, not text)
1265 0 0       0 my $n = $1 ? 4 : 0; # skip leading 4-byte size word if it exists
1266 0 0       0 next if length($buff) < 20 + $n;
1267 0         0 $et->HandleTag($tagTbl, GPSLatitude => Get32s(\$buff, 12+$n) * 180/0x80000000);
1268 0         0 $et->HandleTag($tagTbl, GPSLongitude => Get32s(\$buff, 16+$n) * 180/0x80000000);
1269 0         0 $et->HandleTag($tagTbl, GPSSpeed => Get16u(\$buff, 8+$n) * $mphToKph);
1270 0         0 SetGPSDateTime($et, $tagTbl, $time[$i]);
1271 0         0 next; # all done (don't store/process as text)
1272             }
1273 0 0       0 unless (defined $val) {
1274 0         0 $et->HandleTag($tagTbl, Text => $buff); # just store any other text
1275             }
1276             }
1277 0         0 Process_text($et, \$buff, $tagTbl);
1278              
1279             } elsif ($processByMetaFormat{$type}) {
1280              
1281 4 50       21 if ($$tagTbl{$metaFormat}) {
    0          
1282 4         25 my $tagInfo = $et->GetTagInfo($tagTbl, $metaFormat, \$buff);
1283 4 50 0     32 if ($tagInfo) {
    0          
1284 4         24 FoundSomething($et, $tagTbl, $time[$i], $dur[$i]);
1285 4         16 $$et{ee} = $ee; # need ee information for 'keys'
1286 4         34 $et->HandleTag($tagTbl, $metaFormat, undef,
1287             DataPt => \$buff,
1288             DataPos => 0,
1289             Base => $$start[$i], # (Base must be set for CR3 files)
1290             TagInfo => $tagInfo,
1291             );
1292 4         34 delete $$et{ee};
1293             } elsif ($metaFormat eq 'camm' and $buff =~ /^X/) {
1294             # seen 'camm' metadata in this format (X/Y/Z acceleration and G force? + GPRMC + ?)
1295             # "X0000.0000Y0000.0000Z0000.0000G0000.0000$GPRMC,000125,V,,,,,000.0,,280908,002.1,N*71~, 794021 \x0a"
1296 0         0 FoundSomething($et, $tagTbl, $time[$i], $dur[$i]);
1297 0 0       0 $et->HandleTag($tagTbl, Accelerometer => "$1 $2 $3 $4") if $buff =~ /X(.*?)Y(.*?)Z(.*?)G(.*?)\$/;
1298 0         0 Process_text($et, \$buff, $tagTbl);
1299             }
1300             } elsif ($verbose) {
1301 0         0 $et->VPrint(0, "Unknown $type format ($metaFormat)");
1302             }
1303              
1304             } elsif ($type eq 'gps ') { # (ie. GPSDataList tag)
1305              
1306 0 0       0 if ($buff =~ /^....freeGPS /s) {
1307             # decode "freeGPS " data (Novatek)
1308 0         0 ProcessFreeGPS($et, {
1309             DataPt => \$buff,
1310             DataPos => $$start[$i],
1311             SampleTime => $time[$i],
1312             SampleDuration => $dur[$i],
1313             }, $tagTbl) ;
1314             }
1315              
1316             } elsif ($$tagTbl{$type}) {
1317              
1318 4         23 my $tagInfo = $et->GetTagInfo($tagTbl, $type, \$buff);
1319 4 50       14 if ($tagInfo) {
1320 4         27 FoundSomething($et, $tagTbl, $time[$i], $dur[$i]);
1321 4         22 $et->HandleTag($tagTbl, $type, undef,
1322             DataPt => \$buff,
1323             DataPos => 0,
1324             Base => $$start[$i], # (Base must be set for CR3 files)
1325             TagInfo => $tagInfo,
1326             );
1327             }
1328             }
1329             # generate approximate GPSDateTime if necessary
1330 8 50 33     83 SetGPSDateTime($et, $tagTbl, $time[$i]) if $$et{FoundGPSLatitude} and not $$et{FoundGPSDateTime};
1331             }
1332 8 50       35 if ($verbose) {
1333 0         0 $$et{INDENT} = $oldIndent;
1334 0         0 $et->VPrint(0, "--------------------------\n");
1335             }
1336             # clean up
1337 8         56 $raf->Seek($tell, 0); # restore original file position
1338 8         34 $$et{DOC_NUM} = 0;
1339 8         94 $$et{HandlerType} = $$et{HanderDesc} = '';
1340             }
1341              
1342             #------------------------------------------------------------------------------
1343             # Convert latitude/longitude from DDDMM.MMMM format to decimal degrees
1344             # Inputs: 0) latitude, 1) longitude
1345             # Returns: lat/lon are changed in place
1346             # (note: this method works fine for negative coordinates)
1347             sub ConvertLatLon($$)
1348             {
1349 0     0 0 0 my $deg = int($_[0] / 100); # latitude
1350 0         0 $_[0] = $deg + ($_[0] - $deg * 100) / 60;
1351 0         0 $deg = int($_[1] / 100); # longitude
1352 0         0 $_[1] = $deg + ($_[1] - $deg * 100) / 60;
1353             }
1354              
1355             #------------------------------------------------------------------------------
1356             # Process "freeGPS " data blocks referenced by a 'gps ' (GPSDataList) atom
1357             # Inputs: 0) ExifTool ref, 1) dirInfo ref {DataPt,SampleTime,SampleDuration}, 2) tagTable ref
1358             # Returns: 1 on success (or 0 on unrecognized or "measurement-void" GPS data)
1359             # Notes:
1360             # - also see ProcessFreeGPS2() below for processing of other types of freeGPS blocks
1361             sub ProcessFreeGPS($$$)
1362             {
1363 0     0 0 0 my ($et, $dirInfo, $tagTbl) = @_;
1364 0         0 my $dataPt = $$dirInfo{DataPt};
1365 0         0 my $dirLen = length $$dataPt;
1366 0         0 my ($yr, $mon, $day, $hr, $min, $sec, $stat, $lbl, $ddd);
1367 0         0 my ($lat, $latRef, $lon, $lonRef, $spd, $trk, $alt, @acc, @xtra);
1368              
1369 0 0       0 return 0 if $dirLen < 92;
1370              
1371 0 0 0     0 if (substr($$dataPt,18,8) eq "\xaa\xaa\xf2\xe1\xf0\xee\x54\x54") {
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
1372              
1373             # (this is very similar to the encrypted text format)
1374             # decode encrypted ASCII-based GPS (DashCam Azdome GS63H, ref 5)
1375             # header looks like this in my sample:
1376             # 0000: 00 00 80 00 66 72 65 65 47 50 53 20 05 01 00 00 [....freeGPS ....]
1377             # 0010: 01 03 aa aa f2 e1 f0 ee 54 54 98 9a 9b 92 9a 93 [........TT......]
1378             # 0020: 98 9e 98 98 9e 93 98 92 a6 9f 9f 9c 9d ed fa 8a [................]
1379             # decrypted (from byte 18):
1380             # 0000: 00 00 58 4b 5a 44 fe fe 32 30 31 38 30 39 32 34 [..XKZD..20180924]
1381             # 0010: 32 32 34 39 32 38 0c 35 35 36 37 47 50 20 20 20 [224928.5567GP ]
1382             # 0020: 00 00 00 00 00 03 4e 34 30 34 36 34 33 35 30 57 [......N40464350W]
1383             # 0030: 30 30 37 30 34 30 33 30 38 30 30 30 30 30 30 30 [0070403080000000]
1384             # 0040: 37 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 [7...............]
1385             # [...]
1386             # 00a0: 00 00 00 00 00 00 00 00 00 00 00 00 00 2b 30 39 [.............+09]
1387             # 00b0: 33 2d 30 30 33 2d 30 30 35 00 00 00 00 00 00 00 [3-003-005.......]
1388             # header looks like this for EEEkit gps:
1389             # 0000: 00 00 04 00 66 72 65 65 47 50 53 20 f0 03 00 00 [....freeGPS ....]
1390             # 0010: 01 03 aa aa f2 e1 f0 ee 54 54 98 9a 98 9a 9a 9f [........TT......]
1391             # 0020: 9b 93 9b 9c 98 99 99 9f a6 9a 9a 98 9a 9a 9f 9b [................]
1392             # 0030: 93 9b 9c 98 99 99 9c a9 e4 99 9d 9e 9f 98 9e 9b [................]
1393             # 0040: 9c fd 9b 98 98 98 9f 9f 9a 9a 93 81 9a 9b 9d 9f [................]
1394             # decrypted (from byte 18):
1395             # 0000: 00 00 58 4b 5a 44 fe fe 32 30 32 30 30 35 31 39 [..XKZD..20200519]
1396             # 0010: 31 36 32 33 33 35 0c 30 30 32 30 30 35 31 39 31 [162335.002005191]
1397             # 0020: 36 32 33 33 36 03 4e 33 37 34 35 32 34 31 36 57 [62336.N37452416W]
1398             # 0030: 31 32 32 32 35 35 30 30 39 2b 30 31 37 35 30 31 [122255009+017501]
1399             # 0040: 31 2b 30 31 34 2b 30 30 32 2b 30 32 36 2b 30 31 [1+014+002+026+01]
1400 0         0 my $n = $dirLen - 18;
1401 0 0       0 $n = 0x101 if $n > 0x101;
1402 0         0 my $buf2 = pack 'C*', map { $_ ^ 0xaa } unpack 'C*', substr($$dataPt,18,$n);
  0         0  
1403 0 0       0 if ($et->Options('Verbose') > 1) {
1404 0         0 $et->VPrint(1, '[decrypted freeGPS data]');
1405 0         0 $et->VerboseDump(\$buf2);
1406             }
1407             # (extract longitude as 9 digits, not 8, ref PH)
1408 0 0       0 return 0 unless $buf2 =~ /^.{8}(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2}).(.{15})([NS])(\d{8})([EW])(\d{9})(\d{8})?/s;
1409 0         0 ($yr,$mon,$day,$hr,$min,$sec,$lbl,$latRef,$lat,$lonRef,$lon,$spd) = ($1,$2,$3,$4,$5,$6,$7,$8,$9/1e4,$10,$11/1e4,$12);
1410 0 0       0 if (defined $spd) { # (Azdome)
    0          
1411 0         0 $spd += 0; # remove leading 0's
1412             } elsif ($buf2 =~ /^.{57}([-+]\d{4})(\d{3})/s) { # (EEEkit)
1413             # $alt = $1 + 0; (doesn't look right for my sample, but the Ambarella A12 text has this)
1414 0         0 $spd = $2 + 0;
1415             }
1416 0         0 $lbl =~ s/\0.*//s; $lbl =~ s/\s+$//; # truncate at null and remove trailing spaces
  0         0  
1417 0 0       0 push @xtra, UserLabel => $lbl if length $lbl;
1418             # extract accelerometer data (ref PH)
1419 0 0       0 if ($buf2 =~ /^.{65}(([-+]\d{3})([-+]\d{3})([-+]\d{3})([-+]\d{3})*)/s) {
    0          
1420 0         0 $_ = $1;
1421 0         0 @acc = ($2/100, $3/100, $4/100);
1422 0         0 s/([-+])/ $1/g; s/^ //;
  0         0  
1423 0         0 push @xtra, AccelerometerData => $_;
1424             } elsif ($buf2 =~ /^.{173}([-+]\d{3})([-+]\d{3})([-+]\d{3})/s) { # (Azdome)
1425 0         0 @acc = ($1/100, $2/100, $3/100);
1426             }
1427 0 0       0 $debug and $et->FoundTag(GPSType => '1A');
1428              
1429             } elsif ($$dataPt =~ /^.{52}(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})/s) {
1430              
1431             # decode NMEA-format GPS data (NextBase 512GW dashcam, ref PH)
1432             # header looks like this in my sample:
1433             # 0000: 00 00 80 00 66 72 65 65 47 50 53 20 40 01 00 00 [....freeGPS @...]
1434             # 0010: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 [................]
1435             # 0020: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 [................]
1436 0         0 push @xtra, CameraDateTime => "$1:$2:$3 $4:$5:$6";
1437 0 0       0 if ($$dataPt =~ /\$[A-Z]{2}RMC,(\d{2})(\d{2})(\d+(\.\d*)?),A?,(\d+\.\d+),([NS]),(\d+\.\d+),([EW]),(\d*\.?\d*),(\d*\.?\d*),(\d{2})(\d{2})(\d+)/s) {
1438 0         0 ($lat,$latRef,$lon,$lonRef) = ($5,$6,$7,$8);
1439 0 0       0 $yr = $13 + ($13 >= 70 ? 1900 : 2000);
1440 0         0 ($mon,$day,$hr,$min,$sec) = ($12,$11,$1,$2,$3);
1441 0 0       0 $spd = $9 * $knotsToKph if length $9;
1442 0 0       0 $trk = $10 if length $10;
1443             }
1444 0 0       0 if ($$dataPt =~ /\$[A-Z]{2}GGA,(\d{2})(\d{2})(\d+(\.\d*)?),(\d+\.\d+),([NS]),(\d+\.\d+),([EW]),[1-6]?,(\d+)?,(\.\d+|\d+\.?\d*)?,(-?\d+\.?\d*)?,M?/s) {
1445 0 0       0 ($hr,$min,$sec,$lat,$latRef,$lon,$lonRef) = ($1,$2,$3,$5,$6,$7,$8) unless defined $yr;
1446 0         0 $alt = $11;
1447 0         0 unshift @xtra, GPSSatellites => $9;
1448 0         0 unshift @xtra, GPSDOP => $10;
1449             }
1450 0 0       0 if (defined $lat) {
1451             # extract accelerometer readings if GPS was valid
1452 0         0 @acc = unpack('x68V3', $$dataPt);
1453             # change to signed integer and divide by 256
1454 0 0       0 map { $_ = $_ - 4294967296 if $_ >= 0x80000000; $_ /= 256 } @acc;
  0         0  
  0         0  
1455             }
1456 0 0       0 $debug and $et->FoundTag(GPSType => '1B');
1457              
1458             } elsif ($$dataPt =~ /^.{37}\0\0\0A([NS])([EW])/s) {
1459              
1460             # decode freeGPS from ViofoA119v3 dashcam (similar to Novatek GPS format)
1461             # 0000: 00 00 40 00 66 72 65 65 47 50 53 20 f0 03 00 00 [..@.freeGPS ....]
1462             # 0010: 05 00 00 00 2f 00 00 00 03 00 00 00 13 00 00 00 [..../...........]
1463             # 0020: 09 00 00 00 1b 00 00 00 41 4e 57 00 25 d1 99 45 [........ANW.%..E]
1464             # 0030: f1 47 40 46 66 66 d2 41 85 eb 83 41 00 00 00 00 [.G@Fff.A...A....]
1465 0         0 ($latRef, $lonRef) = ($1, $2);
1466 0         0 ($hr,$min,$sec,$yr,$mon,$day) = unpack('x16V6', $$dataPt);
1467 0 0       0 if ($yr < 2000) {
1468 0         0 $yr += 2000;
1469             } else {
1470             # Kenwood dashcam sometimes stores absolute year and local time
1471             # (but sometimes year since 2000 and UTC time in same video!)
1472 0         0 require Time::Local;
1473 0         0 my $time = Image::ExifTool::TimeLocal($sec,$min,$hr,$day,$mon-1,$yr-1900);
1474 0         0 ($sec,$min,$hr,$day,$mon,$yr) = gmtime($time);
1475 0         0 $yr += 1900;
1476 0         0 ++$mon;
1477 0         0 $et->WarnOnce('Converting GPSDateTime to UTC based on local time zone',1);
1478             }
1479 0         0 SetByteOrder('II');
1480 0         0 $lat = GetFloat($dataPt, 0x2c);
1481 0         0 $lon = GetFloat($dataPt, 0x30);
1482 0         0 $spd = GetFloat($dataPt, 0x34) * $knotsToKph; # (convert knots to km/h)
1483 0         0 $trk = GetFloat($dataPt, 0x38);
1484 0         0 @acc = unpack('x60V3', $$dataPt); # (may be all zeros if not valid)
1485 0 0       0 map { $_ = $_ - 4294967296 if $_ >= 0x80000000; $_ /= 256 } @acc;
  0         0  
  0         0  
1486 0         0 SetByteOrder('MM');
1487 0 0       0 $debug and $et->FoundTag(GPSType => '1C');
1488              
1489             } elsif ($$dataPt =~ /^.{21}\0\0\0A([NS])([EW])/s) {
1490              
1491             # also decode 'gpmd' chunk from Kingslim D4 dashcam videos
1492             # 0000: 0a 00 00 00 0b 00 00 00 07 00 00 00 e5 07 00 00 [................]
1493             # 0010: 06 00 00 00 03 00 00 00 41 4e 57 31 91 52 83 45 [........ANW1.R.E]
1494             # 0020: 15 70 fe c5 29 5c c3 41 ae c7 af 42 00 00 d1 be [.p..)\.A...B....]
1495             # 0030: 00 00 80 3b 00 00 2c 3e 00 00 00 00 00 00 00 00 [...;..,>........]
1496             # 0040: 00 00 00 00 00 00 00 00 00 00 00 00 26 26 26 26 [............&&&&]
1497             # 0050: 4c 49 47 4f 47 50 53 49 4e 46 4f 00 00 00 00 05 [LIGOGPSINFO.....]
1498             # 0060: 01 00 00 00 23 23 23 23 75 00 00 00 c0 22 20 20 [....####u...." ]
1499             # 0070: 20 f0 12 10 12 21 e5 0e 10 12 2f 90 10 13 01 f2 [ ....!..../.....]
1500 0         0 ($latRef, $lonRef) = ($1, $2);
1501 0         0 ($hr,$min,$sec,$yr,$mon,$day) = unpack("V6", $$dataPt);
1502 0         0 SetByteOrder('II');
1503             # lat/lon aren't decoded properly, but spd,trk,acc are
1504 0         0 $lat = GetFloat($dataPt, 0x1c);
1505 0         0 $lon = GetFloat($dataPt, 0x20);
1506 0         0 $et->VPrint(0, sprintf("Raw lat/lon = %.9f %.9f\n", $lat, $lon));
1507 0         0 $et->WarnOnce('GPSLatitude/Longitude encryption is not yet known, so these will be wrong');
1508 0         0 $lat = abs $lat;
1509 0         0 $lon = abs $lon;
1510 0         0 $spd = GetFloat($dataPt, 0x24) * $knotsToKph; # (convert knots to km/h)
1511 0         0 $trk = GetFloat($dataPt, 0x28);
1512 0         0 $acc[0] = GetFloat($dataPt, 0x2c);
1513 0         0 $acc[1] = GetFloat($dataPt, 0x30);
1514 0         0 $acc[2] = GetFloat($dataPt, 0x34);
1515 0         0 SetByteOrder('MM');
1516 0 0       0 $debug and $et->FoundTag(GPSType => '1D');
1517              
1518             } elsif ($$dataPt =~ /^.{60}A\0{3}.{4}([NS])\0{3}.{4}([EW])\0{3}/s) {
1519              
1520             # decode freeGPS from Akaso dashcam
1521             # 0000: 00 00 80 00 66 72 65 65 47 50 53 20 60 00 00 00 [....freeGPS `...]
1522             # 0010: 78 2e 78 78 00 00 00 00 00 00 00 00 00 00 00 00 [x.xx............]
1523             # 0020: 30 30 30 30 30 00 00 00 00 00 00 00 00 00 00 00 [00000...........]
1524             # 0030: 12 00 00 00 2f 00 00 00 19 00 00 00 41 00 00 00 [..../.......A...]
1525             # 0040: 13 b3 ca 44 4e 00 00 00 29 92 fb 45 45 00 00 00 [...DN...)..EE...]
1526             # 0050: d9 ee b4 41 ec d1 d3 42 e4 07 00 00 01 00 00 00 [...A...B........]
1527             # 0060: 0c 00 00 00 01 00 00 00 05 00 00 00 00 00 00 00 [................]
1528 0         0 ($latRef, $lonRef) = ($1, $2);
1529 0         0 ($hr, $min, $sec, $yr, $mon, $day) = unpack('x48V3x28V3', $$dataPt);
1530 0         0 SetByteOrder('II');
1531 0         0 $lat = GetFloat($dataPt, 0x40);
1532 0         0 $lon = GetFloat($dataPt, 0x48);
1533 0         0 $spd = GetFloat($dataPt, 0x50);
1534 0         0 $trk = GetFloat($dataPt, 0x54) + 180; # (why is this off by 180?)
1535 0 0       0 $trk -= 360 if $trk >= 360;
1536 0         0 SetByteOrder('MM');
1537 0 0       0 $debug and $et->FoundTag(GPSType => '1E');
1538              
1539             } elsif ($$dataPt =~ /^.{60}4W`b]S= 140) {
1540              
1541             # 0000: 00 00 40 00 66 72 65 65 47 50 53 20 f0 01 00 00 [..@.freeGPS ....]
1542             # 0010: 5a 58 53 42 4e 58 59 53 00 00 00 00 00 00 00 00 [ZXSBNXYS........]
1543             # 0020: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 [................]
1544             # 0030: 00 00 00 00 00 00 00 00 00 00 00 00 34 57 60 62 [............4W`b]
1545             # 0040: 5d 53 3c 41 44 45 41 41 42 3e 40 40 3c 51 3c 45 []S@@
1546             # 0050: 41 40 43 3e 41 47 49 48 44 3c 5e 3c 40 41 46 43 [A@C>AGIHD<^<@AFC]
1547             # 0060: 42 3e 49 49 40 42 45 3c 55 3c 45 47 3e 45 43 41 [B>II@BEECA]
1548             # decipher $GPRMC by subtracting 16 from each character value
1549 0 0       0 $_ = pack 'C*', map { $_>=16 and $_-=16 } unpack('x60C80', $$dataPt);
  0         0  
1550 0 0       0 return 0 unless /[A-Z]{2}RMC,(\d{2})(\d{2})(\d+(\.\d*)?),A?,(\d*?\d{1,2}\.\d+),([NS]),(\d*?\d{1,2}\.\d+),([EW]),(\d*\.?\d*),(\d*\.?\d*),(\d{2})(\d{2})(\d+)/;
1551 0         0 ($yr,$mon,$day,$hr,$min,$sec,$lat,$latRef,$lon,$lonRef) = ($13,$12,$11,$1,$2,$3,$5,$6,$7,$8);
1552 0 0       0 $yr += ($yr >= 70 ? 1900 : 2000);
1553 0 0       0 $spd = $9 * $knotsToKph if length $9;
1554 0 0       0 $trk = $10 if length $10;
1555 0 0       0 $debug and $et->FoundTag(GPSType => '1F');
1556              
1557             } elsif ($$dataPt =~ /^.{64}[\x01-\x0c]\0{3}[\x01-\x1f]\0{3}A[NS][EW]\0{5}/s) {
1558              
1559             # Akaso V1 dascham
1560             # 0000: 00 00 80 00 66 72 65 65 47 50 53 20 78 00 00 00 [....freeGPS x...]
1561             # 0010: 59 6e 64 41 6b 61 73 6f 43 61 72 00 00 00 00 00 [YndAkasoCar.....]
1562             # 0020: 30 30 30 30 30 00 00 00 00 00 00 00 00 00 00 00 [00000...........]
1563             # 0030: 0e 00 00 00 27 00 00 00 2c 00 00 00 e3 07 00 00 [....'...,.......]
1564             # 0040: 05 00 00 00 1d 00 00 00 41 4e 45 00 00 00 00 00 [........ANE.....]
1565             # 0050: f1 4e 3e 3d 90 df ca 40 e3 50 bf 0b 0b 31 a0 40 [.N>=...@.P...1.@]
1566             # 0060: 4b dc c8 41 9a 79 a7 43 34 58 43 31 4f 37 31 35 [K..A.y.C4XC1O715]
1567             # 0070: 35 31 32 36 36 35 37 35 59 4e 44 53 0d e7 cc f9 [51266575YNDS....]
1568             # 0080: 00 00 00 00 05 00 00 00 00 00 00 00 00 00 00 00 [................]
1569             # Redtiger F7N dashcam
1570             # 0000: 00 00 40 00 66 72 65 65 47 50 53 20 f0 01 00 00 [..@.freeGPS ....]
1571             # 0010: 0a 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 [................]
1572             # 0020: 01 00 00 00 b0 56 50 01 7b 18 68 45 17 02 3f 46 [.....VP.{.hE..?F]
1573             # 0030: 13 00 00 00 01 00 00 00 06 00 00 00 15 00 00 00 [................]
1574             # 0040: 0c 00 00 00 1c 00 00 00 41 4e 57 00 00 00 00 00 [........ANW.....]
1575             # 0050: 80 d4 26 4e 36 11 b5 40 74 b5 15 7b cd 7b f3 40 [..&N6..@t..{.{.@]
1576             # 0060: 0a d7 a3 3d cd 4c 4e 43 38 34 37 41 45 48 31 36 [...=.LNC847AEH16]
1577             # 0070: 33 36 30 38 32 34 35 37 59 53 4b 4a 01 00 00 00 [36082457YSKJ....]
1578             # 0080: ec ff ff ff 00 00 00 00 0e 00 00 00 01 00 00 00 [................]
1579             # 0090: 0a 00 00 00 e5 07 00 00 0c 00 00 00 1c 00 00 00 [................]
1580 0         0 ($hr,$min,$sec,$yr,$mon,$day,$stat,$latRef,$lonRef) =
1581             unpack('x48V6a1a1a1x1', $$dataPt);
1582             # ignore invalid fixes
1583 0 0 0     0 return 0 unless $stat eq 'A' and ($latRef eq 'N' or $latRef eq 'S') and
      0        
      0        
      0        
1584             ($lonRef eq 'E' or $lonRef eq 'W');
1585              
1586 0         0 $et->WarnOnce('GPSLatitude/Longitude encryption is not yet known, so these will be wrong');
1587             # (see https://exiftool.org/forum/index.php?topic=11320.0)
1588              
1589 0         0 SetByteOrder('II');
1590              
1591 0         0 $spd = GetFloat($dataPt, 0x60);
1592 0         0 $trk = GetFloat($dataPt, 0x64) + 180; # (why is this off by 180?)
1593 0         0 $lat = GetDouble($dataPt, 0x50); # latitude is here, but encrypted somehow
1594 0         0 $lon = GetDouble($dataPt, 0x58); # longitude is here, but encrypted somehow
1595 0         0 $ddd = 1; # don't convert until we know what the format is
1596              
1597 0         0 SetByteOrder('MM');
1598             #my $serialNum = substr($$dataPt, 0x68, 20);
1599 0 0       0 $debug and $et->FoundTag(GPSType => '1G');
1600              
1601             } elsif ($$dataPt =~ /^.{12}\xac\0\0\0.{44}(.{72})/s) {
1602              
1603             # EACHPAI dash cam
1604             # 0000: 00 00 80 00 66 72 65 65 47 50 53 20 ac 00 00 00 [....freeGPS ....]
1605             # 0010: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 [................]
1606             # 0020: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 [................]
1607             # 0030: 00 00 00 00 00 00 00 00 00 00 00 00 34 57 60 62 [............4W`b]
1608             # 0040: 5d 53 3c 41 47 45 45 42 42 3e 40 40 40 3c 51 3c []S@@@
1609             # 0050: 44 42 44 40 3e 48 46 43 45 3c 5e 3c 40 48 43 41 [DBD@>HFCE<^<@HCA]
1610             # 0060: 42 3e 46 42 47 48 3c 67 3c 40 3e 40 42 3c 43 3e [B>FBGH@B]
1611             # 0070: 43 41 3c 40 42 40 46 42 40 3c 3c 3c 51 3a 47 46 [CA<@B@FB@<<
1612             # 0080: 00 2a 36 35 00 00 00 00 00 00 00 00 00 00 00 00 [.*65............]
1613              
1614 0         0 $et->WarnOnce("Can't yet decrypt EACHPAI timed GPS", 1);
1615             # (see https://exiftool.org/forum/index.php?topic=5095.msg61266#msg61266)
1616 0         0 return 1;
1617              
1618 0         0 my $time = pack 'C*', map { $_ ^= 0 } unpack 'C*', $1;
  0         0  
1619             # bytes 7-12 are the timestamp in ASCII HHMMSS after xor-ing with 0x70
1620 0         0 substr($time,7,6) = pack 'C*', map { $_ ^= 0x70 } unpack 'C*', substr($time,7,6);
  0         0  
1621             # (other values are currently unknown)
1622 0 0       0 $debug and $et->FoundTag(GPSType => '1H');
1623              
1624             } elsif ($$dataPt =~ /^.{64}A([NS])([EW])\0/s) {
1625              
1626             # Vantrue S1 dashcam
1627             # 0000: 00 00 80 00 66 72 65 65 47 50 53 20 78 00 00 00 [....freeGPS x...]
1628             # 0010: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 [................]
1629             # 0020: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 [................]
1630             # 0030: 68 6f 72 73 6f 6e 74 65 63 68 00 00 00 00 00 00 [horsontech......]
1631             # 0040: 41 4e 45 00 15 00 00 00 07 00 00 00 02 00 00 00 [ANE.............]
1632             # 0050: 03 00 00 00 35 00 00 00 05 00 00 00 4f 74 4c 44 [....5.......OtLD]
1633             # 0060: e2 77 a0 45 89 c1 98 42 71 bd ac 42 02 ab 0d 43 [.w.E...Bq..B...C]
1634             # 0070: 05 00 00 00 7f 00 00 00 07 01 00 00 00 00 00 00 [................]
1635 0         0 ($latRef, $lonRef) = ($1, $2);
1636 0         0 ($yr,$mon,$day,$hr,$min,$sec,@acc) = unpack('x68V6x20V3', $$dataPt);
1637 0 0 0     0 return 0 unless $mon>=1 and $mon<=12 and $day>=1 and $day<=31;
      0        
      0        
1638 0 0       0 $yr += 2000 if $yr < 2000;
1639             # (not sure about acc scaling)
1640 0 0       0 map { $_ = $_ - 4294967296 if $_ >= 0x80000000; $_ /= 1000 } @acc;
  0         0  
  0         0  
1641 0         0 SetByteOrder('II');
1642 0         0 $lon = GetFloat($dataPt, 0x5c);
1643 0         0 $lat = GetFloat($dataPt, 0x60);
1644 0         0 $spd = GetFloat($dataPt, 0x64) * $knotsToKph;
1645 0         0 $trk = GetFloat($dataPt, 0x68);
1646 0         0 $alt = GetFloat($dataPt, 0x6c);
1647 0         0 SetByteOrder('MM');
1648 0 0       0 $debug and $et->FoundTag(GPSType => '1I');
1649              
1650             } else {
1651              
1652             # decode binary GPS format (Viofo A119S, ref 2)
1653             # header looks like this in my sample:
1654             # 0000: 00 00 80 00 66 72 65 65 47 50 53 20 4c 00 00 00 [....freeGPS L...]
1655             # 0010: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 [................]
1656             # 0020: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 [................]
1657             # 0030: 10 00 00 00 2d 00 00 00 14 00 00 00 11 00 00 00 [....-...........]
1658             # 0040: 0c 00 00 00 1f 00 00 00 41 4e 45 00 5d 9a a9 45 [........ANE.]..E]
1659             # 0050: ab 1e e5 44 ec 51 f0 40 b8 5e a5 43 00 00 00 00 [...D.Q.@.^.C....]
1660             # (records are same structure as Type 3 Novatek GPS in ProcessFreeGPS2() below)
1661 0         0 ($hr,$min,$sec,$yr,$mon,$day,$stat,$latRef,$lonRef,$lat,$lon,$spd,$trk) =
1662             unpack('x48V6a1a1a1x1V4', $$dataPt);
1663             # ignore invalid fixes
1664 0 0 0     0 return 0 unless $stat eq 'A' and ($latRef eq 'N' or $latRef eq 'S') and
      0        
      0        
      0        
1665             ($lonRef eq 'E' or $lonRef eq 'W');
1666 0         0 ($lat,$lon,$spd,$trk) = unpack 'f*', pack 'L*', $lat, $lon, $spd, $trk;
1667             # lat/lon also stored as doubles by Transcend Driver Pro 230 (ref PH)
1668 0         0 SetByteOrder('II');
1669 0         0 my ($lat2, $lon2, $alt2) = (
1670             GetDouble($dataPt, 0x70),
1671             GetDouble($dataPt, 0x80),
1672             # GetDouble($dataPt, 0x98), # (don't know what this is)
1673             GetDouble($dataPt,0xa0),
1674             # GetDouble($dataPt,0xa8)) # (don't know what this is)
1675             );
1676 0 0 0     0 if (abs($lat2-$lat) < 0.001 and abs($lon2-$lon) < 0.001) {
1677 0         0 $lat = $lat2;
1678 0         0 $lon = $lon2;
1679 0         0 $alt = $alt2;
1680             }
1681 0         0 SetByteOrder('MM');
1682 0 0       0 $yr += 2000 if $yr < 2000;
1683 0         0 $spd *= $knotsToKph; # convert speed to km/h
1684             # ($trk is not confirmed; may be GPSImageDirection, ref PH)
1685 0 0       0 $debug and $et->FoundTag(GPSType => '1J');
1686             }
1687             #
1688             # save tag values extracted by above code
1689             #
1690 0         0 FoundSomething($et, $tagTbl, $$dirInfo{SampleTime}, $$dirInfo{SampleDuration});
1691             # lat/long are in DDDMM.MMMM format
1692 0 0       0 ConvertLatLon($lat, $lon) unless $ddd;
1693 0 0       0 $sec = '0' . $sec unless $sec =~ /^\d{2}/; # pad integer part of seconds to 2 digits
1694 0 0       0 if (defined $yr) {
    0          
1695 0         0 my $time = sprintf('%.4d:%.2d:%.2d %.2d:%.2d:%sZ',$yr,$mon,$day,$hr,$min,$sec);
1696 0         0 $et->HandleTag($tagTbl, GPSDateTime => $time);
1697             } elsif (defined $hr) {
1698 0         0 my $time = sprintf('%.2d:%.2d:%sZ',$hr,$min,$sec);
1699 0         0 $et->HandleTag($tagTbl, GPSTimeStamp => $time);
1700             }
1701 0 0       0 $et->HandleTag($tagTbl, GPSLatitude => $lat * ($latRef eq 'S' ? -1 : 1));
1702 0 0       0 $et->HandleTag($tagTbl, GPSLongitude => $lon * ($lonRef eq 'W' ? -1 : 1));
1703 0 0       0 $et->HandleTag($tagTbl, GPSAltitude => $alt) if defined $alt;
1704 0 0       0 $et->HandleTag($tagTbl, GPSSpeed => $spd) if defined $spd;
1705 0 0       0 $et->HandleTag($tagTbl, GPSTrack => $trk) if defined $trk;
1706 0         0 while (@xtra) {
1707 0         0 my $tag = shift @xtra;
1708 0         0 $et->HandleTag($tagTbl, $tag => shift @xtra);
1709             }
1710 0 0       0 $et->HandleTag($tagTbl, Accelerometer => \@acc) if @acc;
1711 0         0 return 1;
1712             }
1713              
1714             #------------------------------------------------------------------------------
1715             # Process "freeGPS " data blocks _not_ referenced by a 'gps ' atom
1716             # Inputs: 0) ExifTool ref, 1) dirInfo ref {DataPt,DataPos,DirLen}, 2) tagTable ref
1717             # Returns: 1 on success
1718             # Notes:
1719             # - also see ProcessFreeGPS() above
1720             sub ProcessFreeGPS2($$$)
1721             {
1722 0     0 0 0 my ($et, $dirInfo, $tagTbl) = @_;
1723 0         0 my $dataPt = $$dirInfo{DataPt};
1724 0         0 my $dirLen = $$dirInfo{DirLen};
1725 0         0 my ($yr, $mon, $day, $hr, $min, $sec, $pos, @acc);
1726 0         0 my ($lat, $latRef, $lon, $lonRef, $spd, $trk, $alt, $ddd, $unk);
1727              
1728 0 0       0 return 0 if $dirLen < 82; # minimum size of block with a single GPS record
1729              
1730 0 0 0     0 if (substr($$dataPt,0x45,3) eq 'ATC') {
    0          
    0          
    0          
    0          
1731              
1732             # header looks like this: (sample 1)
1733             # 0000: 00 00 80 00 66 72 65 65 47 50 53 20 38 06 00 00 [....freeGPS 8...]
1734             # 0010: 49 51 53 32 30 31 33 30 33 30 36 42 00 00 00 00 [IQS20130306B....]
1735             # 0020: 4d 61 79 20 31 35 20 32 30 31 35 2c 20 31 39 3a [May 15 2015, 19:]
1736             # (sample 2)
1737             # 0000: 00 00 80 00 66 72 65 65 47 50 53 20 4c 06 00 00 [....freeGPS L...]
1738             # 0010: 32 30 31 33 30 33 31 38 2e 30 31 00 00 00 00 00 [20130318.01.....]
1739             # 0020: 4d 61 72 20 31 38 20 32 30 31 33 2c 20 31 34 3a [Mar 18 2013, 14:]
1740              
1741 0         0 my ($recPos, $lastRecPos, $foundNew);
1742 0         0 my $verbose = $et->Options('Verbose');
1743 0         0 my $dataPos = $$dirInfo{DataPos};
1744 0         0 my $then = $$et{FreeGPS2}{Then};
1745 0 0       0 $then or $then = $$et{FreeGPS2}{Then} = [ (0) x 6 ];
1746             # Loop through records in the ATC-type GPS block until we find the most recent.
1747             # If we have already found one, then we only need to check the first record
1748             # (in case the buffer wrapped around), and the record after the position of
1749             # the last record we found, because the others will be old. Odd, but this
1750             # is the way it is done... I have only seen one new 52-byte record in the
1751             # entire 32 kB block, but the entire device ring buffer (containing 30
1752             # entries in my samples) is stored every time. The code below allows for
1753             # the possibility of missing blocks and multiple new records in a single
1754             # block, but I have never seen this. Note that there may be some earlier
1755             # GPS records at the end of the first block that we will miss decoding, but
1756             # these should (I believe) be before the start of the video
1757 0         0 ATCRec: for ($recPos = 0x30; $recPos + 52 < $dirLen; $recPos += 52) {
1758              
1759 0         0 my $a = substr($$dataPt, $recPos, 52); # isolate a single record
1760             # decrypt record
1761 0         0 my @a = unpack('C*', $a);
1762 0         0 my ($key1, $key2) = @a[0x14, 0x1c];
1763 0         0 $a[$_] ^= $key1 foreach 0x00..0x14, 0x18..0x1b;
1764 0         0 $a[$_] ^= $key2 foreach 0x1c, 0x20..0x32;
1765 0         0 my $b = pack 'C*', @a;
1766             # unpack and validate date/time
1767 0         0 my @now = unpack 'x13C3x28vC2', $b; # (H-1,M,S,Y,m,d)
1768 0         0 $now[0] = ($now[0] + 1) & 0xff; # increment hour
1769 0         0 my $i;
1770 0         0 for ($i=0; $i<@dateMax; ++$i) {
1771 0 0       0 next if $now[$i] <= $dateMax[$i];
1772 0         0 $et->WarnOnce('Invalid GPS date/time');
1773 0         0 next ATCRec; # ignore this record
1774             }
1775             # look for next ATC record in temporal sequence
1776 0         0 foreach $i (3..5, 0..2) {
1777 0 0       0 if ($now[$i] < $$then[$i]) {
1778 0 0       0 last ATCRec if $foundNew;
1779 0         0 last;
1780             }
1781 0 0       0 next if $now[$i] == $$then[$i];
1782             # we found a more recent record -- extract it and remember its location
1783 0 0       0 if ($verbose) {
1784 0         0 $et->VPrint(2, " + [encrypted GPS record]\n");
1785 0         0 $et->VerboseDump(\$a, DataPos => $dataPos + $recPos);
1786 0         0 $et->VPrint(2, " + [decrypted GPS record]\n");
1787 0         0 $et->VerboseDump(\$b);
1788             #my @v = unpack 'H8VVC4V!CA3V!CA3VvvV!vCCCCH4', $b;
1789             #$et->VPrint(2, " + [unpacked: @v]\n");
1790             # values unpacked above (ref PH):
1791             # 0) 0x00 4 bytes - byte 0=1, 1=counts to 255, 2=record index, 3=0 (ref 3)
1792             # 1) 0x04 4 bytes - int32u: bits 0-4=day, 5-8=mon, 9-19=year (ref 3)
1793             # 2) 0x08 4 bytes - int32u: bits 0-5=sec, 6-11=min, 12-16=hour (ref 3)
1794             # 3) 0x0c 1 byte - seen values of 0,1,2 - GPS status maybe?
1795             # 4) 0x0d 1 byte - hour minus 1
1796             # 5) 0x0e 1 byte - minute
1797             # 6) 0x0f 1 byte - second
1798             # 7) 0x10 4 bytes - int32s latitude * 1e7
1799             # 8) 0x14 1 byte - always 0 (used for decryption)
1800             # 9) 0x15 3 bytes - always "ATC"
1801             # 10) 0x18 4 bytes - int32s longitude * 1e7
1802             # 11) 0x1c 1 byte - always 0 (used for decryption)
1803             # 12) 0x1d 3 bytes - always "001"
1804             # 13) 0x20 4 bytes - int32s speed * 100 (m/s)
1805             # 14) 0x24 2 bytes - int16u heading * 100 (-180 to 180 deg)
1806             # 15) 0x26 2 bytes - always zero
1807             # 16) 0x28 4 bytes - int32s altitude * 1000 (ref 3)
1808             # 17) 0x2c 2 bytes - int16u year
1809             # 18) 0x2e 1 byte - month
1810             # 19) 0x2f 1 byte - day
1811             # 20) 0x30 1 byte - unknown
1812             # 21) 0x31 1 byte - always zero
1813             # 22) 0x32 2 bytes - checksum ?
1814             }
1815 0         0 @$then = @now;
1816 0         0 $$et{DOC_NUM} = ++$$et{DOC_COUNT};
1817 0         0 $trk = Get16s(\$b, 0x24) / 100;
1818 0 0       0 $trk += 360 if $trk < 0;
1819 0         0 my $time = sprintf('%.4d:%.2d:%.2d %.2d:%.2d:%.2dZ', @now[3..5, 0..2]);
1820 0         0 $et->HandleTag($tagTbl, GPSDateTime => $time);
1821 0         0 $et->HandleTag($tagTbl, GPSLatitude => Get32s(\$b, 0x10) / 1e7);
1822 0         0 $et->HandleTag($tagTbl, GPSLongitude => Get32s(\$b, 0x18) / 1e7);
1823 0         0 $et->HandleTag($tagTbl, GPSSpeed => Get32s(\$b, 0x20) / 100 * $mpsToKph);
1824 0         0 $et->HandleTag($tagTbl, GPSTrack => $trk);
1825 0         0 $et->HandleTag($tagTbl, GPSAltitude => Get32s(\$b, 0x28) / 1000);
1826 0         0 $lastRecPos = $recPos;
1827 0         0 $foundNew = 1;
1828             # don't skip to location of previous recent record in ring buffer
1829             # since we found a more recent record here
1830 0         0 delete $$et{FreeGPS2}{RecentRecPos};
1831 0         0 last;
1832             }
1833             # skip older records
1834 0         0 my $recentRecPos = $$et{FreeGPS2}{RecentRecPos};
1835 0 0 0     0 $recPos = $recentRecPos if $recentRecPos and $recPos < $recentRecPos;
1836             }
1837             # save position of most recent record (needed when parsing the next freeGPS block)
1838 0         0 $$et{FreeGPS2}{RecentRecPos} = $lastRecPos;
1839 0 0       0 $debug and $et->FoundTag(GPSType => '2A');
1840 0         0 return 1;
1841              
1842             } elsif ($$dataPt =~ /^.{60}A\0.{10}([NS])\0.{14}([EW])\0/s) {
1843              
1844             # header looks like this in my sample:
1845             # 0000: 00 00 80 00 66 72 65 65 47 50 53 20 08 01 00 00 [....freeGPS ....]
1846             # 0010: 32 30 31 33 30 38 31 35 2e 30 31 00 00 00 00 00 [20130815.01.....]
1847             # 0020: 4a 75 6e 20 31 30 20 32 30 31 37 2c 20 31 34 3a [Jun 10 2017, 14:]
1848              
1849             # Type 2 (ref PH):
1850             # 0x30 - int32u hour
1851             # 0x34 - int32u minute
1852             # 0x38 - int32u second
1853             # 0x3c - int32u GPS status ('A' or 'V')
1854             # 0x40 - double latitude (DDMM.MMMMMM)
1855             # 0x48 - int32u latitude ref ('N' or 'S')
1856             # 0x50 - double longitude (DDMM.MMMMMM)
1857             # 0x58 - int32u longitude ref ('E' or 'W')
1858             # 0x60 - double speed (knots)
1859             # 0x68 - double heading (deg)
1860             # 0x70 - int32u year - 2000
1861             # 0x74 - int32u month
1862             # 0x78 - int32u day
1863             # 0x7c - int32s[3] accelerometer * 1000
1864 0         0 ($latRef, $lonRef) = ($1, $2);
1865 0         0 ($hr,$min,$sec,$yr,$mon,$day,@acc) = unpack('x48V3x52V6', $$dataPt);
1866 0 0       0 map { $_ = $_ - 4294967296 if $_ >= 0x80000000; $_ /= 1000 } @acc;
  0         0  
  0         0  
1867 0         0 $lat = GetDouble($dataPt, 0x40);
1868 0         0 $lon = GetDouble($dataPt, 0x50);
1869 0         0 $spd = GetDouble($dataPt, 0x60) * $knotsToKph;
1870 0         0 $trk = GetDouble($dataPt, 0x68);
1871 0 0       0 $debug and $et->FoundTag(GPSType => '2B');
1872              
1873             } elsif ($$dataPt =~ /^.{72}A([NS])([EW])/s) {
1874              
1875             # Type 3 (Novatek GPS, ref 2): (in case it wasn't decoded via 'gps ' atom)
1876             # 0x30 - int32u hour
1877             # 0x34 - int32u minute
1878             # 0x38 - int32u second
1879             # 0x3c - int32u year - 2000
1880             # 0x40 - int32u month
1881             # 0x44 - int32u day
1882             # 0x48 - int8u GPS status ('A' or 'V')
1883             # 0x49 - int8u latitude ref ('N' or 'S')
1884             # 0x4a - int8u longitude ref ('E' or 'W')
1885             # 0x4b - 0
1886             # 0x4c - float latitude (DDMM.MMMMMM)
1887             # 0x50 - float longitude (DDMM.MMMMMM)
1888             # 0x54 - float speed (knots)
1889             # 0x58 - float heading (deg)
1890             # Type 3b, same as above for 0x30-0x4a (ref PH)
1891             # 0x4c - int32s latitude (decimal degrees * 1e7)
1892             # 0x50 - int32s longitude (decimal degrees * 1e7)
1893             # 0x54 - int32s speed (m/s * 100)
1894             # 0x58 - float altitude (m * 1000, NC)
1895 0         0 ($latRef, $lonRef) = ($1, $2);
1896 0         0 ($hr,$min,$sec,$yr,$mon,$day) = unpack('x48V6', $$dataPt);
1897 0 0       0 if (substr($$dataPt, 16, 3) eq 'IQS') {
1898             # Type 3b (ref PH)
1899             # header looks like this in my sample:
1900             # 0000: 00 00 80 00 66 72 65 65 47 50 53 20 4c 00 00 00 [....freeGPS L...]
1901             # 0010: 49 51 53 5f 41 37 5f 32 30 31 35 30 34 31 37 00 [IQS_A7_20150417.]
1902             # 0020: 4d 61 72 20 32 39 20 32 30 31 37 2c 20 31 36 3a [Mar 29 2017, 16:]
1903 0         0 $ddd = 1;
1904 0         0 $lat = abs Get32s($dataPt, 0x4c) / 1e7;
1905 0         0 $lon = abs Get32s($dataPt, 0x50) / 1e7;
1906 0         0 $spd = Get32s($dataPt, 0x54) / 100 * $mpsToKph;
1907 0         0 $alt = GetFloat($dataPt, 0x58) / 1000; # (NC)
1908 0 0       0 $debug and $et->FoundTag(GPSType => '2C');
1909              
1910             } else {
1911             # Type 3 (ref 2)
1912             # (no sample with this format)
1913 0         0 $lat = GetFloat($dataPt, 0x4c);
1914 0         0 $lon = GetFloat($dataPt, 0x50);
1915 0         0 $spd = GetFloat($dataPt, 0x54) * $knotsToKph;
1916 0         0 $trk = GetFloat($dataPt, 0x58);
1917 0 0       0 $debug and $et->FoundTag(GPSType => '2D');
1918             }
1919              
1920             } elsif ($$dataPt =~ /^.{60}A\0.{6}([NS])\0.{6}([EW])\0/s and $dirLen >= 112) {
1921              
1922             # header looks like this in my sample (unknown dashcam, "Anticlock 2 2020_1125_1455_007.MOV"):
1923             # 0000: 00 00 80 00 66 72 65 65 47 50 53 20 68 00 00 00 [....freeGPS h...]
1924             # 0010: 32 30 31 33 30 33 32 35 41 00 00 00 00 00 00 00 [20130325A.......]
1925             # 0020: 41 70 72 20 20 36 20 32 30 31 36 2c 20 31 36 3a [Apr 6 2016, 16:]
1926             # 0030: 0e 00 00 00 38 00 00 00 22 00 00 00 41 00 00 00 [....8..."...A...]
1927             # 0040: 8a 63 24 45 53 00 00 00 9f e6 42 45 45 00 00 00 [.c$ES.....BEE...]
1928             # 0050: 59 c0 04 3f 52 b8 42 41 14 00 00 00 0b 00 00 00 [Y..?R.BA........]
1929             # 0060: 19 00 00 00 06 00 00 00 05 00 00 00 f6 ff ff ff [................]
1930             # 0070: 03 00 00 00 04 00 00 00 00 00 00 00 00 00 00 00 [................]
1931 0         0 ($latRef, $lonRef) = ($1, $2);
1932 0         0 ($hr,$min,$sec,$yr,$mon,$day,@acc) = unpack('x48V3x28V6',$$dataPt);
1933 0 0       0 map { $_ = $_ - 4294967296 if $_ >= 0x80000000; $_ /= 1000 } @acc; # (NC)
  0         0  
  0         0  
1934 0         0 $lat = GetFloat($dataPt, 0x40);
1935 0         0 $lon = GetFloat($dataPt, 0x48);
1936 0         0 $spd = GetFloat($dataPt, 0x50);
1937 0         0 $trk = GetFloat($dataPt, 0x54);
1938 0 0       0 $debug and $et->FoundTag(GPSType => '2E');
1939              
1940             } elsif ($$dataPt =~ /^.{16}A([NS])([EW])\0/s) {
1941              
1942             # INNOVV MP4 video (same format as INNOVV TS)
1943 0         0 while ($$dataPt =~ /(A[NS][EW]\0.{28})/g) {
1944 0         0 my $dat = $1;
1945 0         0 $lat = abs(GetFloat(\$dat, 4)); # (abs just to be safe)
1946 0         0 $lon = abs(GetFloat(\$dat, 8)); # (abs just to be safe)
1947 0         0 $spd = GetFloat(\$dat, 12) * $knotsToKph;
1948 0         0 $trk = GetFloat(\$dat, 16);
1949 0         0 @acc = unpack('x20V3', $dat);
1950 0 0       0 map { $_ = $_ - 4294967296 if $_ >= 0x80000000 } @acc;
  0         0  
1951 0         0 ConvertLatLon($lat, $lon);
1952 0         0 $$et{DOC_NUM} = ++$$et{DOC_COUNT};
1953 0 0       0 $et->HandleTag($tagTbl, GPSLatitude => $lat * (substr($dat,1,1) eq 'S' ? -1 : 1));
1954 0 0       0 $et->HandleTag($tagTbl, GPSLongitude => $lon * (substr($dat,2,1) eq 'W' ? -1 : 1));
1955 0         0 $et->HandleTag($tagTbl, GPSSpeed => $spd);
1956 0         0 $et->HandleTag($tagTbl, GPSTrack => $trk);
1957 0         0 $et->HandleTag($tagTbl, Accelerometer => "@acc");
1958             }
1959 0 0       0 $debug and $et->FoundTag(GPSType => '2F');
1960 0         0 return 1;
1961              
1962             } else {
1963              
1964             # (look for binary GPS as stored by NextBase 512G, ref PH)
1965             # header looks like this in my sample:
1966             # 0000: 00 00 80 00 66 72 65 65 47 50 53 20 78 01 00 00 [....freeGPS x...]
1967             # 0010: 78 2e 78 78 00 00 00 00 00 00 00 00 00 00 00 00 [x.xx............]
1968             # 0020: 30 30 30 30 30 00 00 00 00 00 00 00 00 00 00 00 [00000...........]
1969              
1970             # followed by a number of 32-byte records in this format (big endian!):
1971             # 0x30 - int16u unknown (seen: 0x24 0x53 = "$S")
1972             # 0x32 - int16u speed (m/s * 100)
1973             # 0x34 - int16s heading (deg * 100) (or GPSImgDirection?)
1974             # 0x36 - int16u year
1975             # 0x38 - int8u month
1976             # 0x39 - int8u day
1977             # 0x3a - int8u hour
1978             # 0x3b - int8u min
1979             # 0x3c - int16u sec * 10
1980             # 0x3e - int8u unknown (seen: 2)
1981             # 0x3f - int32s latitude (decimal degrees * 1e7)
1982             # 0x43 - int32s longitude (decimal degrees * 1e7)
1983             # 0x47 - int8u unknown (seen: 16)
1984             # 0x48-0x4f - all zero
1985 0         0 for ($pos=0x32; ; ) {
1986 0         0 ($spd,$trk,$yr,$mon,$day,$hr,$min,$sec,$unk,$lat,$lon) = unpack "x${pos}nnnCCCCnCNN", $$dataPt;
1987             # validate record using date/time
1988 0 0 0     0 last if $yr < 2000 or $yr > 2200 or
      0        
      0        
      0        
      0        
      0        
      0        
      0        
1989             $mon < 1 or $mon > 12 or
1990             $day < 1 or $day > 31 or
1991             $hr > 59 or $min > 59 or $sec > 600;
1992             # change lat/lon to signed integer and divide by 1e7
1993 0 0       0 map { $_ = $_ - 4294967296 if $_ >= 0x80000000; $_ /= 1e7 } $lat, $lon;
  0         0  
  0         0  
1994 0 0       0 $trk -= 0x10000 if $trk >= 0x8000; # make it signed
1995 0         0 $trk /= 100;
1996 0 0       0 $trk += 360 if $trk < 0;
1997 0         0 my $time = sprintf("%.4d:%.2d:%.2d %.2d:%.2d:%04.1fZ", $yr, $mon, $day, $hr, $min, $sec/10);
1998 0         0 $$et{DOC_NUM} = ++$$et{DOC_COUNT};
1999 0         0 $et->HandleTag($tagTbl, GPSDateTime => $time);
2000 0         0 $et->HandleTag($tagTbl, GPSLatitude => $lat);
2001 0         0 $et->HandleTag($tagTbl, GPSLongitude => $lon);
2002 0         0 $et->HandleTag($tagTbl, GPSSpeed => $spd / 100 * $mpsToKph);
2003 0         0 $et->HandleTag($tagTbl, GPSTrack => $trk);
2004 0 0       0 last if $pos += 0x20 > length($$dataPt) - 0x1e;
2005             }
2006 0 0       0 $debug and $et->FoundTag(GPSType => '2G');
2007 0 0       0 return $$et{DOC_NUM} ? 1 : 0; # return 0 if nothing extracted
2008             }
2009             #
2010             # save tag values extracted by above code
2011             #
2012 0 0 0     0 return 0 if $mon < 1 or $mon > 12; # quick sanity check
2013 0         0 $$et{DOC_NUM} = ++$$et{DOC_COUNT};
2014 0 0       0 $yr += 2000 if $yr < 2000;
2015 0         0 my $time = sprintf('%.4d:%.2d:%.2d %.2d:%.2d:%.2dZ', $yr, $mon, $day, $hr, $min, $sec);
2016             # convert from DDMM.MMMMMM to DD.DDDDDD format if necessary
2017 0 0       0 ConvertLatLon($lat, $lon) unless $ddd;
2018 0         0 $et->HandleTag($tagTbl, GPSDateTime => $time);
2019 0 0       0 $et->HandleTag($tagTbl, GPSLatitude => $lat * ($latRef eq 'S' ? -1 : 1));
2020 0 0       0 $et->HandleTag($tagTbl, GPSLongitude => $lon * ($lonRef eq 'W' ? -1 : 1));
2021 0 0       0 $et->HandleTag($tagTbl, GPSSpeed => $spd) if defined $spd; # (now in km/h)
2022 0 0       0 $et->HandleTag($tagTbl, GPSTrack => $trk) if defined $trk;
2023 0 0       0 if (defined $alt) {
2024 0         0 $et->HandleTag($tagTbl, GPSAltitude => $alt);
2025             }
2026 0 0       0 $et->HandleTag($tagTbl, Accelerometer => "@acc") if @acc;
2027 0         0 return 1;
2028             }
2029              
2030              
2031             #------------------------------------------------------------------------------
2032             # Extract embedded information referenced from a track
2033             # Inputs: 0) ExifTool ref, 1) tag name, 2) data ref
2034             sub ParseTag($$$)
2035             {
2036 68     68 0 121 local $_;
2037 68         152 my ($et, $tag, $dataPt) = @_;
2038 68         122 my $dataLen = length $$dataPt;
2039              
2040 68 100 33     637 if ($tag eq 'stsz' or $tag eq 'stz2' and $dataLen > 12) {
    100 66        
    100 66        
    100 33        
    50 66        
    50 66        
    0 0        
    0          
2041             # read the sample sizes
2042 16         56 my ($sz, $num) = unpack('x4N2', $$dataPt);
2043 16         70 my $size = $$et{ee}{size} = [ ];
2044 16 50       48 if ($tag eq 'stsz') {
2045 16 50       48 if ($sz == 0) {
2046 0         0 @$size = ReadValue($dataPt, 12, 'int32u', $num, $dataLen-12);
2047             } else {
2048 16         65 @$size = ($sz) x $num;
2049             }
2050             } else {
2051 0         0 $sz &= 0xff;
2052 0 0 0     0 if ($sz == 4) {
    0          
2053 0         0 my @tmp = ReadValue($dataPt, 12, 'int8u', int(($num+1)/2), $dataLen-12);
2054 0         0 foreach (@tmp) {
2055 0         0 push @$size, $_ >> 4;
2056 0         0 push @$size, $_ & 0xff;
2057             }
2058             } elsif ($sz == 8 || $sz == 16) {
2059 0         0 @$size = ReadValue($dataPt, 12, "int${sz}u", $num, $dataLen-12);
2060             }
2061             }
2062             } elsif ($tag eq 'stco' or $tag eq 'co64' and $dataLen > 8) {
2063             # read the chunk offsets
2064 16         68 my $num = unpack('x4N', $$dataPt);
2065 16         57 my $stco = $$et{ee}{stco} = [ ];
2066 16 50       96 @$stco = ReadValue($dataPt, 8, $tag eq 'stco' ? 'int32u' : 'int64u', $num, $dataLen-8);
2067             } elsif ($tag eq 'stsc' and $dataLen > 8) {
2068             # read the sample-to-chunk box
2069 16         59 my $num = unpack('x4N', $$dataPt);
2070 16 50       62 if ($dataLen >= 8 + $num * 12) {
2071 16         30 my ($i, @stsc);
2072 16         60 for ($i=0; $i<$num; ++$i) {
2073             # list of (first-chunk, samples-per-chunk, sample-description-index)
2074 16         103 push @stsc, [ unpack('x'.(8+$i*12).'N3', $$dataPt) ];
2075             }
2076 16         69 $$et{ee}{stsc} = \@stsc;
2077             }
2078             } elsif ($tag eq 'stts' and $dataLen > 8) {
2079             # read the time-to-sample box
2080 16         57 my $num = unpack('x4N', $$dataPt);
2081 16 50       62 if ($dataLen >= 8 + $num * 8) {
2082 16         102 $$et{ee}{stts} = [ unpack('x8N'.($num*2), $$dataPt) ];
2083             }
2084             } elsif ($tag eq 'avcC') {
2085             # read the AVC compressor configuration
2086 0 0       0 $$et{ee}{avcC} = $$dataPt if $dataLen >= 7; # (minimum length is 7)
2087             } elsif ($tag eq 'JPEG') {
2088 4         31 $$et{ee}{JPEG} = $$dataPt;
2089             } elsif ($tag eq 'gps ' and $dataLen > 8) {
2090             # decode Novatek 'gps ' box (ref 2)
2091 0         0 my $num = Get32u($dataPt, 4);
2092 0 0       0 $num = int(($dataLen - 8) / 8) if $num * 8 + 8 > $dataLen;
2093 0         0 my $start = $$et{ee}{start} = [ ];
2094 0         0 my $size = $$et{ee}{size} = [ ];
2095 0         0 my $i;
2096 0         0 for ($i=0; $i<$num; ++$i) {
2097 0         0 push @$start, Get32u($dataPt, 8 + $i * 8);
2098 0         0 push @$size, Get32u($dataPt, 12 + $i * 8);
2099             }
2100 0         0 $$et{HandlerType} = $tag; # fake handler type
2101 0         0 ProcessSamples($et); # we have all we need to process sample data now
2102             } elsif ($tag eq 'GPS ') {
2103 0         0 my $pos = 0;
2104 0         0 my $tagTbl = GetTagTable('Image::ExifTool::QuickTime::Stream');
2105 0         0 SetByteOrder('II');
2106 0         0 while ($pos + 36 < $dataLen) {
2107 0         0 my $dat = substr($$dataPt, $pos, 36);
2108 0 0       0 last if $dat eq "\x0" x 36;
2109 0         0 my @a = unpack 'VVVVaVaV', $dat;
2110 0         0 $$et{DOC_NUM} = ++$$et{DOC_COUNT};
2111             # 0=1, 1=1, 2=secs, 3=?
2112 0         0 SetGPSDateTime($et, $tagTbl, $a[2]);
2113 0         0 my $lat = $a[5] / 1e3;
2114 0         0 my $lon = $a[7] / 1e3;
2115 0         0 ConvertLatLon($lat, $lon);
2116 0 0       0 $lat = -abs($lat) if $a[4] eq 'S';
2117 0 0       0 $lon = -abs($lon) if $a[6] eq 'W';
2118 0         0 $et->HandleTag($tagTbl, GPSLatitude => $lat);
2119 0         0 $et->HandleTag($tagTbl, GPSLongitude => $lon);
2120 0         0 $et->HandleTag($tagTbl, GPSSpeed => $a[3] / 1e3);
2121 0         0 $pos += 36;
2122             }
2123 0         0 SetByteOrder('MM');
2124 0         0 delete $$et{DOC_NUM};
2125             }
2126             }
2127              
2128             #------------------------------------------------------------------------------
2129             # Process Yuneec 'tx3g' sbtl metadata (ref PH)
2130             # Inputs: 0) ExifTool object ref, 1) dirInfo ref, 2) tag table ref
2131             # Returns: 1 on success
2132             sub Process_tx3g($$$)
2133             {
2134 0     0 0 0 my ($et, $dirInfo, $tagTablePtr) = @_;
2135 0         0 my $dataPt = $$dirInfo{DataPt};
2136 0 0       0 return 0 if length $$dataPt < 2;
2137 0         0 pos($$dataPt) = 2; # skip 2-byte length word
2138 0         0 $et->VerboseDir('tx3g', undef, length($$dataPt)-2);
2139 0         0 $et->HandleTag($tagTablePtr, 'Text', substr($$dataPt, 2));
2140 0 0       0 if ($$dataPt =~ /^..\w{3} (\d{4})-(\d{2})-(\d{2}) (\d{2}:\d{2}:\d{2}) ?([-+])(\d{2}):?(\d{2})$/s) {
2141 0         0 $et->HandleTag($tagTablePtr, 'DateTime', "$1:$2:$3 $4$5$6:$7");
2142             } else {
2143 0         0 $et->HandleTag($tagTablePtr, $1, $2) while $$dataPt =~ /(\w+):([^:]*[^:\s])(\s|$)/sg;
2144             }
2145 0         0 return 1;
2146             }
2147              
2148             #------------------------------------------------------------------------------
2149             # Process GM 'marl' ctbx metadata (ref PH)
2150             # Inputs: 0) ExifTool object ref, 1) dirInfo ref, 2) tag table ref
2151             # Returns: 1 on success
2152             sub Process_marl($$$)
2153             {
2154 0     0 0 0 my ($et, $dirInfo, $tagTablePtr) = @_;
2155 0         0 my $dataPt = $$dirInfo{DataPt};
2156 0 0       0 return 0 if length $$dataPt < 2;
2157              
2158             # 8-byte records:
2159             # byte 0 seems to be tag ID (0=timestamp in sec * 1e7)
2160             # bytes 1-3 seem to be 24-bit signed integer (unknown meaning)
2161             # bytes 4-7 are an int32u value, usually a multiple of 10000
2162              
2163 0         0 $et->WarnOnce("Can't yet decode timed GM data", 1);
2164             # (see https://exiftool.org/forum/index.php?topic=11335.msg61393#msg61393)
2165 0         0 return 1;
2166             }
2167              
2168             #------------------------------------------------------------------------------
2169             # Process QuickTime 'mebx' timed metadata
2170             # Inputs: 0) ExifTool ref, 1) dirInfo ref, 2) tag table ref
2171             # Returns: 1 on success
2172             # - uses tag ID keys stored in the ExifTool ee data member by a previous call to SaveMetaKeys
2173             sub Process_mebx($$$)
2174             {
2175 0     0 0 0 my ($et, $dirInfo, $tagTbl) = @_;
2176 0 0       0 my $ee = $$et{ee} or return 0;
2177 0 0       0 return 0 unless $$ee{'keys'};
2178 0         0 my $dataPt = $$dirInfo{DataPt};
2179              
2180             # parse using information from 'keys' table (eg. Apple iPhone7+ hevc 'Core Media Data Handler')
2181 0         0 $et->VerboseDir('mebx', undef, length $$dataPt);
2182 0         0 my ($pos, $len);
2183 0         0 for ($pos=0; $pos+8
2184 0         0 $len = Get32u($dataPt, $pos);
2185 0 0 0     0 last if $len < 8 or $pos + $len > length $$dataPt;
2186 0         0 my $id = substr($$dataPt, $pos+4, 4);
2187 0         0 my $info = $$ee{'keys'}{$id};
2188 0 0       0 if ($info) {
2189 0         0 my $tag = $$info{TagID};
2190 0 0       0 unless ($$tagTbl{$tag}) {
2191 0 0       0 next unless $tag =~ /^[-\w.]+$/;
2192             # create info for tags with reasonable id's
2193 0         0 my $name = $tag;
2194 0         0 $name =~ s/[-.](.)/\U$1/g;
2195 0         0 AddTagToTable($tagTbl, $tag, { Name => ucfirst($name) });
2196             }
2197 0         0 my $val = ReadValue($dataPt, $pos+8, $$info{Format}, undef, $len-8);
2198             $et->HandleTag($tagTbl, $tag, $val,
2199             DataPt => $dataPt,
2200             Base => $$dirInfo{Base},
2201 0         0 Start => $pos + 8,
2202             Size => $len - 8,
2203             );
2204             } else {
2205 0         0 $et->WarnOnce('No key information for mebx ID ' . PrintableTagID($id,1));
2206             }
2207             }
2208 0         0 return 1;
2209             }
2210              
2211             #------------------------------------------------------------------------------
2212             # Process QuickTime '3gf' timed metadata (ref PH, Pittasoft Blackvue dashcam)
2213             # Inputs: 0) ExifTool ref, 1) dirInfo ref, 2) tag table ref
2214             # Returns: 1 on success
2215             sub Process_3gf($$$)
2216             {
2217 0     0 0 0 my ($et, $dirInfo, $tagTbl) = @_;
2218 0         0 my $dataPt = $$dirInfo{DataPt};
2219 0         0 my $dirLen = $$dirInfo{DirLen};
2220 0         0 my $recLen = 10; # 10-byte record length
2221 0         0 $et->VerboseDir('3gf', undef, $dirLen);
2222 0 0 0     0 if ($dirLen > $recLen and not $et->Options('ExtractEmbedded')) {
2223 0         0 $dirLen = $recLen;
2224 0         0 EEWarn($et);
2225             }
2226 0         0 my $pos;
2227 0         0 for ($pos=0; $pos+$recLen<=$dirLen; $pos+=$recLen) {
2228 0         0 $$et{DOC_NUM} = ++$$et{DOC_COUNT};
2229 0         0 my $tc = Get32u($dataPt, $pos);
2230 0 0       0 last if $tc == 0xffffffff;
2231 0         0 my ($x, $y, $z) = (Get16s($dataPt, $pos+4)/10, Get16s($dataPt, $pos+6)/10, Get16s($dataPt, $pos+8)/10);
2232 0         0 $et->HandleTag($tagTbl, TimeCode => $tc / 1000);
2233 0         0 $et->HandleTag($tagTbl, Accelerometer => "$x $y $z");
2234             }
2235 0         0 delete $$et{DOC_NUM};
2236 0         0 return 1;
2237             }
2238              
2239             #------------------------------------------------------------------------------
2240             # Process DuDuBell M1 dashcam / VSYS M6L 'gps0' atom (ref PH)
2241             # Inputs: 0) ExifTool ref, 1) dirInfo ref, 2) tag table ref
2242             # Returns: 1 on success
2243             sub Process_gps0($$$)
2244             {
2245 0     0 0 0 my ($et, $dirInfo, $tagTbl) = @_;
2246 0         0 my $dataPt = $$dirInfo{DataPt};
2247 0         0 my $dirLen = $$dirInfo{DirLen};
2248 0         0 my $recLen = 32; # 32-byte record length
2249 0         0 $et->VerboseDir('gps0', undef, $dirLen);
2250 0         0 SetByteOrder('II');
2251 0 0 0     0 if ($dirLen > $recLen and not $et->Options('ExtractEmbedded')) {
2252 0         0 $dirLen = $recLen;
2253 0         0 EEWarn($et);
2254             }
2255 0         0 my $pos;
2256 0         0 for ($pos=0; $pos+$recLen<=$dirLen; $pos+=$recLen) {
2257 0         0 $$et{DOC_NUM} = ++$$et{DOC_COUNT};
2258             # lat/long are in DDDMM.MMMM format
2259 0         0 my $lat = GetDouble($dataPt, $pos);
2260 0         0 my $lon = GetDouble($dataPt, $pos+8);
2261 0 0 0     0 next if abs($lat) > 9000 or abs($lon) > 18000;
2262 0         0 ConvertLatLon($lat, $lon);
2263 0         0 my @a = unpack('C*', substr($$dataPt, $pos+22, 6)); # unpack date/time
2264 0         0 $a[0] += 2000;
2265 0         0 $et->HandleTag($tagTbl, GPSDateTime => sprintf("%.4d:%.2d:%.2d %.2d:%.2d:%.2dZ", @a));
2266 0         0 $et->HandleTag($tagTbl, GPSLatitude => $lat);
2267 0         0 $et->HandleTag($tagTbl, GPSLongitude => $lon);
2268 0         0 $et->HandleTag($tagTbl, GPSSpeed => Get16u($dataPt, $pos+0x14));
2269 0         0 $et->HandleTag($tagTbl, GPSTrack => Get8u($dataPt, $pos+0x1c) * 2); # (NC)
2270 0         0 $et->HandleTag($tagTbl, GPSAltitude => Get32s($dataPt, $pos + 0x10));
2271             # yet to be decoded:
2272             # 0x1d - int8u[3] seen: "1 1 0"
2273             }
2274 0         0 delete $$et{DOC_NUM};
2275 0         0 SetByteOrder('MM');
2276 0         0 return 1;
2277             }
2278              
2279             #------------------------------------------------------------------------------
2280             # Process DuDuBell M1 dashcam / VSYS M6L 'gsen' atom (ref PH)
2281             # Inputs: 0) ExifTool ref, 1) dirInfo ref, 2) tag table ref
2282             # Returns: 1 on success
2283             sub Process_gsen($$$)
2284             {
2285 0     0 0 0 my ($et, $dirInfo, $tagTbl) = @_;
2286 0         0 my $dataPt = $$dirInfo{DataPt};
2287 0         0 my $dirLen = $$dirInfo{DirLen};
2288 0         0 my $recLen = 3; # 3-byte record length
2289 0         0 $et->VerboseDir('gsen', undef, $dirLen);
2290 0 0 0     0 if ($dirLen > $recLen and not $et->Options('ExtractEmbedded')) {
2291 0         0 $dirLen = $recLen;
2292 0         0 EEWarn($et);
2293             }
2294 0         0 my $pos;
2295 0         0 for ($pos=0; $pos+$recLen<=$dirLen; $pos+=$recLen) {
2296 0         0 $$et{DOC_NUM} = ++$$et{DOC_COUNT};
2297 0         0 my @acc = map { $_ /= 16 } unpack "x${pos}c3", $$dataPt;
  0         0  
2298 0         0 $et->HandleTag($tagTbl, Accelerometer => "@acc");
2299             # (there are no associated timestamps, but these are sampled at 5 Hz in my test video)
2300             }
2301 0         0 delete $$et{DOC_NUM};
2302 0         0 return 1;
2303             }
2304              
2305             #------------------------------------------------------------------------------
2306             # Process RIFF-format trailer written by Auto-Vox dashcam (ref PH)
2307             # Inputs: 0) ExifTool ref, 1) dirInfo ref, 2) tag table ref
2308             # Returns: 1 on success
2309             # Note: This trailer is basically RIFF chunks added to a QuickTime-format file (augh!),
2310             # but there are differences in the record formats so we can't just call
2311             # ProcessRIFF to process the gps0 and gsen atoms using the routines above
2312             sub ProcessRIFFTrailer($$$)
2313             {
2314 0     0 0 0 my ($et, $dirInfo, $tagTbl) = @_;
2315 0         0 my $raf = $$dirInfo{RAF};
2316 0         0 my $verbose = $et->Options('Verbose');
2317 0         0 my ($buff, $pos);
2318 0         0 SetByteOrder('II');
2319 0         0 for (;;) {
2320 0 0       0 last unless $raf->Read($buff, 8) == 8;
2321 0         0 my ($tag, $len) = unpack('a4V', $buff);
2322 0 0       0 last if $tag eq "\0\0\0\0";
2323 0 0 0     0 unless ($tag =~ /^[\w ]{4}/ and $len < 0x2000000) {
2324 0         0 $et->Warn('Bad RIFF trailer');
2325 0         0 last;
2326             }
2327 0 0       0 $raf->Read($buff, $len) == $len or $et->Warn("Truncated $tag record in RIFF trailer"), last;
2328 0 0       0 if ($verbose) {
2329 0         0 $et->VPrint(0, " - RIFF trailer '${tag}' ($len bytes)\n");
2330 0 0       0 $et->VerboseDump(\$buff, Addr => $raf->Tell() - $len) if $verbose > 2;
2331 0         0 $$et{INDENT} .= '| ';
2332 0 0       0 $et->VerboseDir($tag, undef, $len) if $tag =~ /^(gps0|gsen)$/;
2333             }
2334 0 0       0 if ($tag eq 'gps0') {
    0          
2335             # (similar to record decoded in Process_gps0, but with some differences)
2336             # 0000: 41 49 54 47 74 46 94 f6 c6 c5 b4 40 34 a2 b4 37 [AITGtF.....@4..7]
2337             # 0010: f8 7b 8a 40 ff ff 00 00 38 00 77 0a 1a 0c 12 28 [.{.@....8.w....(]
2338             # 0020: 8d 01 02 40 29 07 00 00 [...@)...]
2339             # 0x00 - undef[4] 'AITG'
2340             # 0x04 - double latitude (always positive)
2341             # 0x0c - double longitude (always positive)
2342             # 0x14 - ? seen hex "ff ff 00 00" (altitude in Process_gps0 record below)
2343             # 0x18 - int16u speed in knots (different than km/hr in Process_gps0)
2344             # 0x1a - int8u[6] yr-1900,mon,day,hr,min,sec (different than -2000 in Process_gps0)
2345             # 0x20 - int8u direction in degrees / 2
2346             # 0x21 - int8u guessing that this is 1=N, 2=S - PH
2347             # 0x22 - int8u guessing that this is 1=E, 2=W - PH
2348             # 0x23 - ? seen hex "40"
2349             # 0x24 - in32u time since start of video (ms)
2350 0         0 my $recLen = 0x28;
2351 0         0 for ($pos=0; $pos+$recLen<$len; $pos+=$recLen) {
2352 0 0       0 substr($buff, $pos, 4) eq 'AITG' or $et->Warn('Unrecognized gps0 record'), last;
2353 0         0 $$et{DOC_NUM} = ++$$et{DOC_COUNT};
2354             # lat/long are in DDDMM.MMMM format
2355 0         0 my $lat = GetDouble(\$buff, $pos+4);
2356 0         0 my $lon = GetDouble(\$buff, $pos+12);
2357 0 0 0     0 $et->Warn('Bad gps0 record') and last if abs($lat) > 9000 or abs($lon) > 18000;
      0        
2358 0         0 ConvertLatLon($lat, $lon);
2359 0 0       0 $lat = -$lat if Get8u(\$buff, $pos+0x21) == 2; # wild guess
2360 0 0       0 $lon = -$lon if Get8u(\$buff, $pos+0x22) == 2; # wild guess
2361 0         0 my @a = unpack('C*', substr($buff, $pos+26, 6)); # unpack date/time
2362 0         0 $a[0] += 1900; # (different than Proces_gps0)
2363 0         0 $et->HandleTag($tagTbl, SampleTime => Get32u(\$buff, $pos + 0x24) / 1000);
2364 0         0 $et->HandleTag($tagTbl, GPSDateTime => sprintf("%.4d:%.2d:%.2d %.2d:%.2d:%.2dZ", @a));
2365 0         0 $et->HandleTag($tagTbl, GPSLatitude => $lat);
2366 0         0 $et->HandleTag($tagTbl, GPSLongitude => $lon);
2367 0         0 $et->HandleTag($tagTbl, GPSSpeed => Get16u(\$buff, $pos+0x18) * $knotsToKph);
2368 0         0 $et->HandleTag($tagTbl, GPSTrack => Get8u(\$buff, $pos+0x20) * 2);
2369             }
2370             } elsif ($tag eq 'gsen') {
2371             # (similar to record decoded in Process_gsen)
2372             # 0000: 41 49 54 53 1a 0d 05 ff c8 00 00 00 [AITS........]
2373             # 0x00 - undef[4] 'AITS'
2374             # 0x04 - int8s[3] accelerometer readings
2375             # 0x07 - ? seen hex "ff"
2376             # 0x08 - in32u time since start of video (ms)
2377 0         0 my $recLen = 0x0c;
2378 0         0 for ($pos=0; $pos+$recLen<$len; $pos+=$recLen) {
2379 0 0       0 substr($buff, $pos, 4) eq 'AITS' or $et->Warn('Unrecognized gsen record'), last;
2380 0         0 $$et{DOC_NUM} = ++$$et{DOC_COUNT};
2381 0         0 my @acc = map { $_ /= 24 } unpack('x'.($pos+4).'c3', $buff);
  0         0  
2382 0         0 $et->HandleTag($tagTbl, SampleTime => Get32u(\$buff, $pos + 8) / 1000);
2383             # 0=+Up, 1=+Right, 3=+Forward (calibration of 24 counts/g is a wild guess - PH)
2384 0         0 $et->HandleTag($tagTbl, Accelerometer => "@acc");
2385             }
2386             }
2387             # also seen, but not decoded:
2388             # gpsa (8 bytes): hex "01 20 00 00 08 03 02 08 "
2389             # gsea (20 bytes): all zeros
2390 0 0       0 $$et{INDENT} = substr($$et{INDENT}, 0, -2) if $verbose;
2391             }
2392 0         0 delete $$et{DOC_NUM};
2393 0         0 SetByteOrder('MM');
2394 0         0 return 1;
2395             }
2396              
2397             #------------------------------------------------------------------------------
2398             # Process 'gps ' atom containing NMEA from Pittasoft Blackvue dashcam (ref PH)
2399             # Inputs: 0) ExifTool object ref, 1) dirInfo ref, 2) tag table ref
2400             # Returns: 1 on success
2401             sub ProcessNMEA($$$)
2402             {
2403 0     0 0 0 my ($et, $dirInfo, $tagTbl) = @_;
2404 0         0 my $dataPt = $$dirInfo{DataPt};
2405 0         0 my ($rtnVal, %fix);
2406             # parse only RMC and GGA sentence [with leading timecode] for now
2407 0         0 for (;;) {
2408 0         0 my ($tc, $type, $tim);
2409 0 0       0 if ($$dataPt =~ /(?:\[(\d+)\])?\$[A-Z]{2}(RMC|GGA),(\d{2}\d{2}\d+(\.\d*)?),/g) {
2410 0         0 ($tc, $type, $tim) = ($1, $2, $3);
2411             }
2412             # write out last fix now if complete
2413             # (use the GPS timestamps because they may be different for the same timecode)
2414 0 0 0     0 if ($fix{tim} and (not $tim or $fix{tim} != $tim)) {
      0        
2415 0 0 0     0 if ($fix{dat} and defined $fix{lat} and defined $fix{lon}) {
      0        
2416 0         0 my $sampleTime;
2417 0 0 0     0 $sampleTime = ($fix{tc} - $$et{StartTime}) / 1000 if $fix{tc} and $$et{StartTime};
2418 0         0 FoundSomething($et, $tagTbl, $sampleTime);
2419 0         0 $et->HandleTag($tagTbl, GPSDateTime => $fix{dat});
2420 0         0 $et->HandleTag($tagTbl, GPSLatitude => $fix{lat});
2421 0         0 $et->HandleTag($tagTbl, GPSLongitude => $fix{lon});
2422 0 0       0 $et->HandleTag($tagTbl, GPSSpeed => $fix{spd} * $knotsToKph) if defined $fix{spd};
2423 0 0       0 $et->HandleTag($tagTbl, GPSTrack => $fix{trk}) if defined $fix{trk};
2424 0 0       0 $et->HandleTag($tagTbl, GPSAltitude => $fix{alt}) if defined $fix{alt};
2425 0 0       0 $et->HandleTag($tagTbl, GPSSatellites=> $fix{nsats}+0) if defined $fix{nsats};
2426 0 0       0 $et->HandleTag($tagTbl, GPSDOP => $fix{hdop}) if defined $fix{hdop};
2427             }
2428 0         0 undef %fix;
2429             }
2430 0 0       0 $fix{tim} = $tim or last;
2431 0         0 my $pos = pos($$dataPt);
2432 0         0 pos($$dataPt) = $pos - length($tim) - 1; # rewind to re-parse time
2433             # (parsing of NMEA strings copied from Geotag.pm)
2434 0 0 0     0 if ($type eq 'RMC' and
    0 0        
2435             $$dataPt =~ /\G(\d{2})(\d{2})(\d+(\.\d*)?),A?,(\d*?)(\d{1,2}\.\d+),([NS]),(\d*?)(\d{1,2}\.\d+),([EW]),(\d*\.?\d*),(\d*\.?\d*),(\d{2})(\d{2})(\d+)/g)
2436             {
2437 0 0       0 my $year = $15 + ($15 >= 70 ? 1900 : 2000);
2438 0         0 $fix{tc} = $tc; # use timecode of RMC sentence
2439 0         0 $fix{dat} = sprintf('%.4d:%.2d:%.2d %.2d:%.2d:%sZ',$year,$14,$13,$1,$2,$3);
2440 0 0 0     0 $fix{lat} = (($5 || 0) + $6/60) * ($7 eq 'N' ? 1 : -1);
2441 0 0 0     0 $fix{lon} = (($8 || 0) + $9/60) * ($10 eq 'E' ? 1 : -1);
2442 0 0       0 $fix{spd} = $11 if length $11;
2443 0 0       0 $fix{trk} = $12 if length $12;
2444             } elsif ($type eq 'GGA' and
2445             $$dataPt =~ /\G(\d{2})(\d{2})(\d+(\.\d*)?),(\d*?)(\d{1,2}\.\d+),([NS]),(\d*?)(\d{1,2}\.\d+),([EW]),[1-6]?,(\d+)?,(\.\d+|\d+\.?\d*)?,(-?\d+\.?\d*)?,M?/g)
2446             {
2447 0 0 0     0 $fix{lat} = (($5 || 0) + $6/60) * ($7 eq 'N' ? 1 : -1);
2448 0 0 0     0 $fix{lon} = (($8 || 0) + $9/60) * ($10 eq 'E' ? 1 : -1);
2449 0         0 @fix{qw(nsats hdop alt)} = ($11,$12,$13);
2450             } else {
2451 0         0 pos($$dataPt) = $pos; # continue searching from our last match
2452             }
2453             }
2454 0         0 delete $$et{DOC_NUM};
2455 0         0 return $rtnVal;
2456             }
2457              
2458             #------------------------------------------------------------------------------
2459             # Process 'gps ' or 'udat' atom possibly containing NMEA (ref PH)
2460             # Inputs: 0) ExifTool object ref, 1) dirInfo ref, 2) tag table ref
2461             # Returns: 1 on success
2462             sub ProcessGPSLog($$$)
2463             {
2464 0     0 0 0 my ($et, $dirInfo, $tagTbl) = @_;
2465 0         0 my $dataPt = $$dirInfo{DataPt};
2466 0         0 my ($rtnVal, @a);
2467              
2468             # try NMEA format first
2469 0 0       0 return 1 if ProcessNMEA($et,$dirInfo,$tagTbl);
2470              
2471             # DENVER ACG-8050WMK2 format looks like this:
2472             # 210318073213[1][N][52200970][E][006362321][+00152][100][00140][C000000]+000+000+000+000+000+000+000+000+000+000+000+000+000+000+000+000+000+000
2473             # YYMMDDHHMMSS A? NS lat EW lon alt kph dir kCal accel
2474 0         0 while ($$dataPt =~ /\b(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})\[1\]\[([NS])\]\[(\d{8})\]\[([EW])\]\[(\d{9})\]\[([-+]?\d*)\]\[(\d*)\]\[(\d*)\]\[C?(\d*)\](([-+]\d{3})+)/g) {
2475 0         0 my $lat = substr( $8,0,2) + substr( $8,2) / 600000;
2476 0         0 my $lon = substr($10,0,3) + substr($10,3) / 600000;
2477 0         0 $$et{DOC_NUM} = ++$$et{DOC_COUNT};
2478 0         0 $et->HandleTag($tagTbl, GPSDateTime => "20$1:$2:$3 $4:$5:$6Z");
2479 0 0       0 $et->HandleTag($tagTbl, GPSLatitude => $lat * ($7 eq 'S' ? -1 : 1));
2480 0 0       0 $et->HandleTag($tagTbl, GPSLongitude => $lon * ($9 eq 'W' ? -1 : 1));
2481 0 0       0 $et->HandleTag($tagTbl, GPSAltitude => $11 / 10) if length $11;
2482 0 0       0 $et->HandleTag($tagTbl, GPSSpeed => $12 + 0) if length $12;
2483 0 0       0 $et->HandleTag($tagTbl, GPSTrack => $13 + 0) if length $13;
2484 0 0       0 $et->HandleTag($tagTbl, KiloCalories => $14 / 10) if length $14;
2485 0 0       0 $et->HandleTag($tagTbl, Accelerometer=> $15) if length $15;
2486 0         0 $rtnVal = 1;
2487             }
2488 0         0 delete $$et{DOC_NUM};
2489 0         0 return $rtnVal;
2490             }
2491              
2492             #------------------------------------------------------------------------------
2493             # Process TomTom Bandit Action Cam TTAD atom (ref PH)
2494             # Inputs: 0) ExifTool object ref, 1) dirInfo ref, 2) tag table ref
2495             # Returns: 1 on success
2496             my %ttLen = ( # lengths of known TomTom records
2497             0 => 12, # angular velocity (NC)
2498             1 => 4, # ?
2499             2 => 12, # ?
2500             3 => 12, # accelerometer (NC)
2501             # (haven't seen a record 4 yet)
2502             5 => 92, # GPS
2503             0xff => 4, # timecode
2504             );
2505             sub ProcessTTAD($$$)
2506             {
2507 0     0 0 0 my ($et, $dirInfo, $tagTbl) = @_;
2508 0         0 my $dataPt = $$dirInfo{DataPt};
2509 0         0 my $dirLen = $$dirInfo{DirLen};
2510 0         0 my $pos = 76;
2511              
2512 0 0       0 return 0 if $dirLen < $pos;
2513              
2514 0         0 $et->VerboseDir('TTAD', undef, $dirLen);
2515 0         0 SetByteOrder('II');
2516              
2517 0         0 my $eeOpt = $et->Options('ExtractEmbedded');
2518 0         0 my $unknown = $et->Options('Unknown');
2519 0         0 my $found = 0;
2520 0         0 my $sampleTime = 0;
2521 0         0 my $resync = 1;
2522 0         0 my $skipped = 0;
2523 0         0 my $warned;
2524              
2525 0         0 while ($pos < $dirLen) {
2526             # get next record type
2527 0         0 my $type = Get8u($dataPt, $pos++);
2528             # resync if necessary by skipping data until next timecode record
2529 0 0 0     0 if ($resync and $type != 0xff) {
2530 0 0       0 ++$skipped > 0x100 and $et->Warn('Unrecognized or bad TTAD data', 1), last;
2531 0         0 next;
2532             }
2533 0 0       0 unless ($ttLen{$type}) {
2534             # skip unknown records
2535 0 0       0 $et->Warn("Unknown TTAD record type $type",1) unless $warned;
2536 0         0 $resync = $warned = 1;
2537 0         0 ++$skipped;
2538 0         0 next;
2539             }
2540 0 0       0 last if $pos + $ttLen{$type} > $dirLen;
2541 0 0       0 if ($type == 0xff) { # timecode?
2542 0         0 my $tm = Get32u($dataPt, $pos);
2543             # validate timecode if skipping unknown data
2544 0 0       0 if ($resync) {
2545 0 0 0     0 if ($tm < $sampleTime or $tm > $sampleTime + 250) {
2546 0         0 ++$skipped;
2547 0         0 next;
2548             }
2549 0         0 undef $resync;
2550 0         0 $skipped = 0;
2551             }
2552 0         0 $pos += $ttLen{$type};
2553 0         0 $sampleTime = $tm;
2554 0         0 next;
2555             }
2556 0 0       0 unless ($eeOpt) {
2557             # only extract one of each type without -ee option
2558 0 0       0 $found & (1 << $type) and $pos += $ttLen{$type}, next;
2559 0         0 $found |= (1 << $type);
2560             }
2561 0 0 0     0 if ($type == 0 or $type == 3) {
    0          
    0          
2562             # (these are both just educated guesses - PH)
2563 0         0 FoundSomething($et, $tagTbl, $sampleTime / 1000);
2564 0         0 my @a = map { Get32s($dataPt,$pos+4*$_) / 1000 } 0..2;
  0         0  
2565 0 0       0 $et->HandleTag($tagTbl, ($type ? 'Accelerometer' : 'AngularVelocity') => "@a");
2566             } elsif ($type == 5) {
2567             # example records unpacked with 'dVddddVddddv*'
2568             # datetime ? spd ele lat lon ? trk ? ? ? ? ? ? ? ? ?
2569             # 2019:03:05 07:52:58.999Z 3 0.02 242 48.0254203 7.8497567 0 45.69 13.34 17.218 17.218 0 0 0 32760 5 0
2570             # 2019:03:05 07:52:59.999Z 3 0.14 242 48.0254203 7.8497567 0 45.7 12.96 15.662 15.662 0 0 0 32760 5 0
2571             # 2019:03:05 07:53:00.999Z 3 0.67 243.78 48.0254584 7.8497907 0 50.93 9.16 10.84 10.84 0 0 0 32760 5 0
2572             # (I think "5" may be the number of satellites. seen: 5,6,7 - PH)
2573 0         0 FoundSomething($et, $tagTbl, $sampleTime / 1000);
2574 0         0 my $t = GetDouble($dataPt, $pos);
2575 0         0 $et->HandleTag($tagTbl, GPSDateTime => Image::ExifTool::ConvertUnixTime($t,undef,3) . 'Z');
2576 0         0 $et->HandleTag($tagTbl, GPSLatitude => GetDouble($dataPt, $pos+0x1c));
2577 0         0 $et->HandleTag($tagTbl, GPSLongitude => GetDouble($dataPt, $pos+0x24));
2578 0         0 $et->HandleTag($tagTbl, GPSAltitude => GetDouble($dataPt, $pos+0x14));
2579 0         0 $et->HandleTag($tagTbl, GPSSpeed => GetDouble($dataPt, $pos+0x0c) * $mpsToKph);
2580 0         0 $et->HandleTag($tagTbl, GPSTrack => GetDouble($dataPt, $pos+0x30));
2581 0 0       0 if ($unknown) {
2582 0         0 my @a = map { GetDouble($dataPt, $pos+0x38+8*$_) } 0..2;
  0         0  
2583 0         0 $et->HandleTag($tagTbl, Unknown03 => "@a");
2584             }
2585             } elsif ($type < 3) {
2586             # as yet unknown:
2587             # 1 - int32s[1]? (values around 98k)
2588             # 2 - int32s[3] (values like "806 8124 4323" -- probably something * 1000 again)
2589 0 0       0 if ($unknown) {
2590 0         0 FoundSomething($et, $tagTbl, $sampleTime / 1000);
2591 0 0       0 my $n = $type == 1 ? 0 : 2;
2592 0         0 my @a = map { Get32s($dataPt,$pos+4*$_) } 0..$n;
  0         0  
2593 0         0 $et->HandleTag($tagTbl, "Unknown0$type" => "@a");
2594             }
2595             } else {
2596 0         0 $et->WarnOnce("Unknown TTAD record type $type",1);
2597             }
2598             # without -ee, stop after we find types 0,3,5 (ie. bitmask 0x29)
2599 0 0 0     0 $eeOpt or ($found & 0x29) != 0x29 or EEWarn($et), last;
2600 0         0 $pos += $ttLen{$type};
2601             }
2602 0         0 SetByteOrder('MM');
2603 0         0 delete $$et{DOC_NUM};
2604 0         0 return 1;
2605             }
2606              
2607             #------------------------------------------------------------------------------
2608             # Extract information from Insta360 trailer (INSV and INSP files) (ref PH)
2609             # Inputs: 0) ExifTool ref, 1) Optional dirInfo ref for returning trailer info
2610             # Returns: true on success
2611             sub ProcessInsta360($;$)
2612             {
2613 4     4 0 11 local $_;
2614 4         14 my ($et, $dirInfo) = @_;
2615 4         13 my $raf = $$et{RAF};
2616 4 50 0     17 my $offset = $dirInfo ? $$dirInfo{Offset} || 0 : 0;
2617 4         10 my $buff;
2618              
2619 4 50 33     24 return 0 unless $raf->Seek(-78-$offset, 2) and $raf->Read($buff, 78) == 78 and
      33        
2620             substr($buff,-32) eq "8db42d694ccc418790edff439fe026bf"; # check magic number
2621              
2622 0         0 my $verbose = $et->Options('Verbose');
2623 0         0 my $tagTbl = GetTagTable('Image::ExifTool::QuickTime::Stream');
2624 0         0 my $fileEnd = $raf->Tell();
2625 0         0 my $trailerLen = unpack('x38V', $buff);
2626 0 0       0 $trailerLen > $fileEnd and $et->Warn('Bad Insta360 trailer size'), return 0;
2627 0 0       0 if ($dirInfo) {
2628 0         0 $$dirInfo{DirLen} = $trailerLen;
2629 0         0 $$dirInfo{DataPos} = $fileEnd - $trailerLen;
2630 0 0       0 if ($$dirInfo{OutFile}) {
2631 0 0 0     0 if ($$et{DEL_GROUP}{Insta360}) {
    0 0        
2632 0         0 ++$$et{CHANGED};
2633             # just copy the trailer when writing
2634             } elsif ($trailerLen > $fileEnd or not $raf->Seek($$dirInfo{DataPos}, 0) or
2635 0         0 $raf->Read(${$$dirInfo{OutFile}}, $trailerLen) != $trailerLen)
2636             {
2637 0         0 return 0;
2638             } else {
2639 0         0 return 1;
2640             }
2641             }
2642 0 0 0     0 $et->DumpTrailer($dirInfo) if $verbose or $$et{HTML_DUMP};
2643             }
2644 0 0       0 unless ($et->Options('ExtractEmbedded')) {
2645             # can arrive here when reading Insta360 trailer on JPEG image (INSP file)
2646 0         0 $et->WarnOnce('Use ExtractEmbedded option to extract timed metadata from Insta360 trailer',3);
2647 0         0 return 1;
2648             }
2649              
2650 0         0 my $unknown = $et->Options('Unknown');
2651             # position relative to end of trailer (avoids using large offsets for files > 2 GB)
2652 0         0 my $epos = -78-$offset;
2653 0         0 my ($i, $p);
2654 0         0 $$et{SET_GROUP0} = 'Trailer';
2655 0         0 $$et{SET_GROUP1} = 'Insta360';
2656 0         0 SetByteOrder('II');
2657             # loop through all records in the trailer, from last to first
2658 0         0 for (;;) {
2659 0         0 my ($id, $len) = unpack('vV', $buff);
2660 0 0       0 ($epos -= $len) + $trailerLen < 0 and last;
2661 0 0       0 $raf->Seek($epos, 2) or last;
2662 0         0 my $dlen = $insvDataLen{$id};
2663 0 0       0 if ($verbose) {
2664 0         0 $et->VPrint(0, sprintf("Insta360 Record 0x%x (offset 0x%x, %d bytes):\n", $id, $fileEnd + $epos, $len));
2665             }
2666             # there are 2 types of record 0x300:
2667             # 1. 56 byte records
2668             # 0000: 4a f7 02 00 00 00 00 00 00 00 00 00 00 1e e7 3f [J..............?]
2669             # 0010: 00 00 00 00 00 b2 ef bf 00 00 00 00 00 70 c1 bf [.............p..]
2670             # 0020: 00 00 00 e0 91 5c 8c bf 00 00 00 20 8f ff 87 bf [.....\..... ....]
2671             # 0030: 00 00 00 00 88 7f c9 bf
2672             # 2. 20 byte records
2673             # 0000: c1 d8 d9 0b 00 00 00 00 f5 83 14 80 df 7f fe 7f [................]
2674             # 0010: fe 7f 01 80
2675 0 0       0 if ($id == 0x300) {
2676 0 0 0     0 if ($len % 20 and not $len % 56) {
    0 0        
2677 0         0 $dlen = 56;
2678             } elsif ($len % 56 and not $len % 20) {
2679 0         0 $dlen = 20;
2680             } else {
2681 0 0       0 if ($raf->Read($buff, 20) == 20) {
2682 0 0       0 if (substr($buff, 16, 3) eq "\0\0\0") {
2683 0         0 $dlen = 56;
2684             } else {
2685 0         0 $dlen = 20;
2686             }
2687             }
2688 0 0       0 $raf->Seek($epos, 2) or last;
2689             }
2690             }
2691             # limit the number of records we read if necessary
2692 0 0 0     0 if ($dlen and $insvLimit{$id} and $len > $insvLimit{$id}[1] * $dlen and
      0        
      0        
2693             $et->Warn("Insta360 $insvLimit{$id}[0] data is huge. Processing only the first $insvLimit{$id}[1] records",2))
2694             {
2695 0         0 $len = $insvLimit{$id}[1] * $dlen;
2696             }
2697 0 0       0 $raf->Read($buff, $len) == $len or last;
2698 0 0       0 $et->VerboseDump(\$buff) if $verbose > 2;
2699 0 0       0 if ($dlen) {
    0          
2700 0 0       0 if ($len % $dlen) {
    0          
    0          
    0          
    0          
2701 0         0 $et->Warn(sprintf('Unexpected Insta360 record 0x%x length',$id));
2702             } elsif ($id == 0x300) {
2703 0         0 for ($p=0; $p<$len; $p+=$dlen) {
2704 0         0 $$et{DOC_NUM} = ++$$et{DOC_COUNT};
2705 0         0 my @a;
2706 0 0       0 if ($dlen == 56) {
2707 0         0 @a = map { GetDouble(\$buff, $p + 8 * $_) } 1..6;
  0         0  
2708             } else {
2709 0         0 @a = unpack("x${p}x8v6", $buff);
2710 0         0 map { $_ = ($_ - 0x8000) / 1000 } @a;
  0         0  
2711             }
2712 0         0 $et->HandleTag($tagTbl, TimeCode => sprintf('%.3f', Get64u(\$buff, $p) / 1000));
2713 0         0 $et->HandleTag($tagTbl, Accelerometer => "@a[0..2]"); # (NC)
2714 0         0 $et->HandleTag($tagTbl, AngularVelocity => "@a[3..5]"); # (NC)
2715             }
2716             } elsif ($id == 0x400) {
2717 0         0 for ($p=0; $p<$len; $p+=$dlen) {
2718 0         0 $$et{DOC_NUM} = ++$$et{DOC_COUNT};
2719 0         0 $et->HandleTag($tagTbl, TimeCode => sprintf('%.3f', Get64u(\$buff, $p) / 1000));
2720 0         0 $et->HandleTag($tagTbl, ExposureTime => GetDouble(\$buff, $p + 8)); #6
2721             }
2722             } elsif ($id == 0x600) { #6
2723 0         0 for ($p=0; $p<$len; $p+=$dlen) {
2724 0         0 $$et{DOC_NUM} = ++$$et{DOC_COUNT};
2725 0         0 $et->HandleTag($tagTbl, VideoTimeStamp => sprintf('%.3f', Get64u(\$buff, $p) / 1000));
2726             }
2727             } elsif ($id == 0x700) {
2728 0         0 for ($p=0; $p<$len; $p+=$dlen) {
2729 0         0 my $tmp = substr($buff, $p, $dlen);
2730 0         0 my @a = unpack('VVvaa8aa8aa8a8a8', $tmp);
2731 0 0       0 next unless $a[3] eq 'A'; # (ignore void fixes)
2732 0 0 0     0 unless (($a[5] eq 'N' or $a[5] eq 'S') and # (quick validation)
      0        
      0        
2733             ($a[7] eq 'E' or $a[7] eq 'W' or
2734             # (odd, but I've seen "O" instead of "W". Perhaps
2735             # when the language is french? ie. "Ouest"?)
2736             $a[7] eq 'O'))
2737             {
2738 0         0 $et->Warn('Unrecognized INSV GPS format');
2739 0         0 last;
2740             }
2741 0         0 $$et{DOC_NUM} = ++$$et{DOC_COUNT};
2742 0         0 $a[$_] = GetDouble(\$a[$_], 0) foreach 4,6,8,9,10;
2743 0 0       0 $a[4] = -abs($a[4]) if $a[5] eq 'S'; # (abs just in case it was already signed)
2744 0 0       0 $a[6] = -abs($a[6]) if $a[7] ne 'E';
2745 0         0 $et->HandleTag($tagTbl, GPSDateTime => Image::ExifTool::ConvertUnixTime($a[0]) . 'Z');
2746 0         0 $et->HandleTag($tagTbl, GPSLatitude => $a[4]);
2747 0         0 $et->HandleTag($tagTbl, GPSLongitude => $a[6]);
2748 0         0 $et->HandleTag($tagTbl, GPSSpeed => $a[8] * $mpsToKph);
2749 0         0 $et->HandleTag($tagTbl, GPSTrack => $a[9]);
2750 0         0 $et->HandleTag($tagTbl, GPSAltitude => $a[10]);
2751 0 0       0 $et->HandleTag($tagTbl, Unknown02 => "@a[1,2]") if $unknown; # millisecond counter (https://exiftool.org/forum/index.php?topic=9884.msg65143#msg65143)
2752             }
2753             }
2754             } elsif ($id == 0x101) {
2755 0         0 my $tagTablePtr = GetTagTable('Image::ExifTool::QuickTime::INSV_MakerNotes');
2756 0         0 for ($i=0, $p=0; $i<4; ++$i) {
2757 0 0       0 last if $p + 2 > $len;
2758 0         0 my ($t, $n) = unpack("x${p}CC", $buff);
2759 0 0       0 last if $p + 2 + $n > $len;
2760 0         0 my $val = substr($buff, $p+2, $n);
2761 0         0 $et->HandleTag($tagTablePtr, $t, $val);
2762 0         0 $p += 2 + $n;
2763             }
2764             }
2765 0 0       0 ($epos -= 6) + $trailerLen < 0 and last; # step back to previous record
2766 0 0       0 $raf->Seek($epos, 2) or last;
2767 0 0       0 $raf->Read($buff, 6) == 6 or last;
2768             }
2769 0         0 $$et{DOC_NUM} = 0;
2770 0         0 SetByteOrder('MM');
2771 0         0 delete $$et{SET_GROUP0};
2772 0         0 delete $$et{SET_GROUP1};
2773 0         0 return 1;
2774             }
2775              
2776             #------------------------------------------------------------------------------
2777             # Process Garmin GPS 'uuid' atom (ref PH)
2778             # Inputs: 0) ExifTool object ref, 1) dirInfo ref, 2) tag table ref
2779             # Returns: 1 on success
2780             # Note: This format is used by the Garmin DriveAssist 51, but the DriveAssist 50
2781             # uses a completely different format. :(
2782             sub ProcessGarminGPS($$$)
2783             {
2784 0     0 0 0 my ($et, $dirInfo, $tagTbl) = @_;
2785 0         0 my $dataPt = $$dirInfo{DataPt};
2786 0         0 my $dataLen = length $$dataPt;
2787 0         0 my $pos = 33;
2788 0         0 my $epoch = (66 * 365 + 17) * 24 * 3600; # time is relative to Jan 1, 1904
2789 0         0 my $scl = 180 / (32768 * 65536); # scaling factor for lat/lon
2790 0         0 $et->VerboseDir('GarminGPS');
2791 0         0 while ($pos + 20 <= $dataLen) {
2792 0         0 $$et{DOC_NUM} = ++$$et{DOC_COUNT};
2793 0         0 my $time = Image::ExifTool::ConvertUnixTime(Get32u($dataPt, $pos) - $epoch) . 'Z';
2794 0         0 my $lat = Get32s($dataPt, $pos + 12) * $scl;
2795 0         0 my $lon = Get32s($dataPt, $pos + 16) * $scl;
2796 0         0 my $spd = Get16u($dataPt, $pos + 4); # (in mph)
2797 0         0 $et->HandleTag($tagTbl, 'GPSDateTime', $time);
2798 0         0 $et->HandleTag($tagTbl, 'GPSLatitude', $lat);
2799 0         0 $et->HandleTag($tagTbl, 'GPSLongitude', $lon);
2800 0         0 $et->HandleTag($tagTbl, 'GPSSpeed', $spd);
2801 0         0 $et->HandleTag($tagTbl, 'GPSSpeedRef', 'M');
2802 0         0 $pos += 20;
2803             }
2804 0         0 delete $$et{DOC_NUM};
2805 0         0 return 1;
2806             }
2807              
2808             #------------------------------------------------------------------------------
2809             # Process 360Fly 'uuid' atom containing sensor data
2810             # (ref https://github.com/JamesHeinrich/getID3/blob/master/getid3/module.audio-video.quicktime.php)
2811             # Inputs: 0) ExifTool object ref, 1) dirInfo ref, 2) tag table ref
2812             # Returns: 1 on success
2813             sub Process360Fly($$$)
2814             {
2815 0     0 0 0 my ($et, $dirInfo, $tagTbl) = @_;
2816 0         0 my $dataPt = $$dirInfo{DataPt};
2817 0         0 my $dataLen = length $$dataPt;
2818 0         0 my $pos = 16;
2819 0         0 my $lastTime = -1;
2820 0         0 my $streamTbl = GetTagTable('Image::ExifTool::QuickTime::Stream');
2821 0         0 while ($pos + 32 <= $dataLen) {
2822 0         0 my $type = ord substr $$dataPt, $pos, 1;
2823 0         0 my $time = Get64u($dataPt, $pos + 2); # (only valid for some types)
2824 0 0       0 if ($$tagTbl{$type}) {
2825 0 0       0 if ($time != $lastTime) {
2826 0         0 $$et{DOC_NUM} = ++$$et{DOC_COUNT};
2827 0         0 $lastTime = $time;
2828             }
2829             }
2830 0         0 $et->HandleTag($tagTbl, $type, undef, DataPt => $dataPt, Start => $pos, Size => 32);
2831             # synthesize GPSDateTime from the timestamp for GPS records
2832 0 0       0 SetGPSDateTime($et, $streamTbl, $time / 1e6) if $type == 5;
2833 0         0 $pos += 32;
2834             }
2835 0         0 delete $$et{DOC_NUM};
2836 0         0 return 1;
2837             }
2838              
2839             #------------------------------------------------------------------------------
2840             # Process GPS from Vantrue N2S dashcam
2841             # Inputs: 0) ExifTool object ref, 1) dirInfo ref, 2) tag table ref
2842             # Returns: 1 on success
2843             sub ProcessFMAS($$$)
2844             {
2845 0     0 0 0 my ($et, $dirInfo, $tagTbl) = @_;
2846 0         0 my $dataPt = $$dirInfo{DataPt};
2847 0 0 0     0 return 0 unless $$dataPt =~ /^FMAS\0\0\0\0.{72}SAMM.{36}A/s and length($$dataPt) >= 160;
2848 0         0 $et->VerboseDir('FMAS', undef, length($$dataPt));
2849             # 0000: 46 4d 41 53 00 00 00 00 00 00 00 00 00 00 00 00 [FMAS............]
2850             # 0010: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 [................]
2851             # 0020: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 [................]
2852             # 0030: 02 08 01 08 06 08 02 04 07 02 06 00 00 00 00 00 [................]
2853             # 0040: 00 00 00 00 00 00 00 00 4f 46 4e 49 4d 4d 41 53 [........OFNIMMAS]
2854             # 0050: 53 41 4d 4d 01 00 00 00 00 00 00 00 00 00 00 00 [SAMM............]
2855             # 0060: e5 07 09 18 08 00 22 00 02 00 00 00 a1 82 8a bf [......".........]
2856             # 0070: 89 23 8e bd 0b 2c 30 bc 41 57 4e 51 16 00 a1 01 [.#...,0.AWNQ....]
2857             # 0080: 29 26 27 0c 4b 00 49 00 00 00 00 00 00 00 00 00 [)&'.K.I.........]
2858             # 0090: 00 00 00 00 00 00 00 00 00 52 00 00 00 00 00 00 [.........R......]
2859 0         0 my @a = unpack('x96vCCCCCCx16AAACCCvCCvvv',$$dataPt);
2860 0         0 SetByteOrder('II');
2861 0         0 my $acc = ReadValue($dataPt, 0x6c, 'float', 3); # (looks like Z comes first in my sample)
2862 0         0 my $lon = $a[10] + ($a[11] + $a[13]/6000) / 60; # (why zero byte at $a[12]?)
2863 0         0 my $lat = $a[14] + ($a[15] + $a[16]/6000) / 60;
2864 0         0 $et->HandleTag($tagTbl, GPSDateTime => sprintf('%.4d:%.2d:%.2d %.2d:%.2d:%.2d', @a[0..5]));
2865 0 0       0 $et->HandleTag($tagTbl, GPSLatitude => $lat * ($a[9] eq 'S' ? -1 : 1));
2866 0 0       0 $et->HandleTag($tagTbl, GPSLongitude => $lon * ($a[8] eq 'W' ? -1 : 1));
2867 0         0 $et->HandleTag($tagTbl, GPSSpeed => $a[17] * $mphToKph); # convert mph -> kph
2868 0         0 $et->HandleTag($tagTbl, GPSTrack => $a[18]);
2869 0         0 $et->HandleTag($tagTbl, Accelerometer=> $acc);
2870 0         0 SetByteOrder('MM');
2871 0         0 return 1;
2872             }
2873              
2874             #------------------------------------------------------------------------------
2875             # Scan media data for "freeGPS" metadata if not found already (ref PH)
2876             # Inputs: 0) ExifTool ref
2877             sub ScanMediaData($)
2878             {
2879 4     4 0 12 my $et = shift;
2880 4 50       19 my $raf = $$et{RAF} or return;
2881 4         14 my ($tagTbl, $oldByteOrder, $verbose, $buff, $dataLen);
2882 4         16 my ($pos, $buf2) = (0, '');
2883              
2884             # don't rescan for freeGPS if we already found embedded metadata
2885 4         23 my $dataPos = $$et{MediaDataOffset};
2886 4 50 33     41 if ($dataPos and not $$et{DOC_COUNT}) {
2887 0         0 $dataLen = $$et{MediaDataSize};
2888 0 0       0 if ($dataLen) {
2889 0 0       0 if ($raf->Seek($dataPos, 0)) {
2890 0         0 $$et{FreeGPS2} = { }; # initialize variable space for FreeGPS2()
2891             } else {
2892 0         0 undef $dataLen;
2893             }
2894             }
2895             }
2896              
2897             # loop through 'mdat' media data looking for GPS information
2898 4         19 while ($dataLen) {
2899 0 0       0 last if $pos + $gpsBlockSize > $dataLen;
2900 0 0       0 last unless $raf->Read($buff, $gpsBlockSize);
2901 0 0       0 $buff = $buf2 . $buff if length $buf2;
2902 0 0       0 last if length $buff < $gpsBlockSize;
2903             # look for "freeGPS " block
2904             # (found on an absolute 0x8000-byte boundary in all of my samples,
2905             # but allow for any alignment when searching)
2906 0 0       0 if ($buff !~ /\0..\0freeGPS /sg) { # (seen ".." = "\0\x80","\x01\0")
    0          
2907 0         0 $buf2 = substr($buff,-12);
2908 0         0 $pos += length($buff)-12;
2909             # in all of my samples the first freeGPS block is within 2 MB of the start
2910             # of the mdat, so limit the scan to the first 20 MB to be fast and safe
2911 0 0 0     0 next if $tagTbl or $pos < 20e6;
2912 0         0 last;
2913             } elsif (not $tagTbl) {
2914             # initialize variables for extracting metadata from this block
2915 0         0 $tagTbl = GetTagTable('Image::ExifTool::QuickTime::Stream');
2916 0         0 $verbose = $$et{OPTIONS}{Verbose};
2917 0         0 $oldByteOrder = GetByteOrder();
2918 0         0 SetByteOrder('II');
2919 0         0 $et->VPrint(0, "---- Extract Embedded ----\n");
2920 0         0 $$et{INDENT} .= '| ';
2921             }
2922 0 0       0 if (pos($buff) > 12) {
2923 0         0 $pos += pos($buff) - 12;
2924 0         0 $buff = substr($buff, pos($buff) - 12);
2925             }
2926             # make sure we have the full freeGPS record
2927 0         0 my $len = unpack('N', $buff);
2928 0 0       0 if ($len < 12) {
2929 0         0 $len = 12;
2930             } else {
2931 0         0 my $more = $len - length($buff);
2932 0 0       0 if ($more > 0) {
2933 0 0       0 last unless $raf->Read($buf2, $more) == $more;
2934 0         0 $buff .= $buf2;
2935             }
2936 0 0       0 if ($verbose) {
2937 0         0 $et->VerboseDir('GPS', undef, $len);
2938 0         0 $et->VerboseDump(\$buff, DataPos => $pos + $dataPos);
2939             }
2940 0         0 my $dirInfo = { DataPt => \$buff, DataPos => $pos + $dataPos, DirLen => $len };
2941 0         0 ProcessFreeGPS2($et, $dirInfo, $tagTbl);
2942             }
2943 0         0 $pos += $len;
2944 0         0 $buf2 = substr($buff, $len);
2945             }
2946 4 50       15 if ($tagTbl) {
2947 0         0 $$et{DOC_NUM} = 0; # reset DOC_NUM after extracting embedded metadata
2948 0         0 $et->VPrint(0, "--------------------------\n");
2949 0         0 SetByteOrder($oldByteOrder);
2950 0         0 $$et{INDENT} = substr $$et{INDENT}, 0, -2;
2951             }
2952             # process Insta360 trailer if it exists
2953 4         26 ProcessInsta360($et);
2954             }
2955              
2956             1; # end
2957              
2958             __END__