source: main/trunk/greenstone2/perllib/cpan/Image/ExifTool/QuickTimeStream.pl@ 34921

Last change on this file since 34921 was 34921, checked in by anupama, 3 years ago

Committing the improvements to EmbeddedMetaPlugin's processing of Keywords vs other metadata fields. Keywords were literally stored as arrays of words rather than phrases in PDFs (at least in Diego's sample PDF), whereas other meta fields like Subjects and Creators stored them as arrays of phrases. To get both to work, Kathy updated EXIF to a newer version, to retrieve the actual EXIF values stored in the PDF. And Kathy and Dr Bainbridge came up with a new option that I added called apply_join_before_split_to_metafields that's a regex which can list the metadata fields to apply the join_before_split to and whcih previously always got applied to all metadata fields. Now it's applied to any *Keywords metafields by default, as that's the metafield we have experience of that behaves differently to the others, as it stores by word instead of phrases. Tested on Diego's sample PDF. Diego has double-checked it to works on his sample PDF too, setting the split char to ; and turning on the join_before_split and leaving apply_join_before_split_to_metafields at its default of .*Keywords. File changes are strings.properties for the tooltip, the plugin introducing the option and working with it and Kathy's EXIF updates affecting cpan/File and cpan/Image.

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