File Coverage

blib/lib/Image/ExifTool/QuickTimeStream.pl
Criterion Covered Total %
statement 121 1158 10.4
branch 53 766 6.9
condition 29 326 8.9
subroutine 8 27 29.6
pod 0 24 0.0
total 211 2301 9.1


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