File Coverage

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


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