File Coverage

blib/lib/Image/ExifTool/QuickTimeStream.pl
Criterion Covered Total %
statement 133 1317 10.1
branch 60 890 6.7
condition 31 358 8.6
subroutine 8 30 26.6
pod 0 27 0.0
total 232 2622 8.8


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