File Coverage

blib/lib/Image/ExifTool/QuickTimeStream.pl
Criterion Covered Total %
statement 145 1544 9.3
branch 72 1050 6.8
condition 36 470 7.6
subroutine 7 35 20.0
pod 0 32 0.0
total 260 3131 8.3


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