1 | #------------------------------------------------------------------------------
|
---|
2 | # File: QuickTimeStream.pl
|
---|
3 | #
|
---|
4 | # Description: Extract embedded information from QuickTime media data
|
---|
5 | #
|
---|
6 | # Revisions: 2018-01-03 - P. Harvey Created
|
---|
7 | #
|
---|
8 | # References: 1) https://developer.apple.com/library/content/documentation/QuickTime/QTFF/QTFFChap3/qtff3.html#//apple_ref/doc/uid/TP40000939-CH205-SW130
|
---|
9 | # 2) http://sergei.nz/files/nvtk_mp42gpx.py
|
---|
10 | # 3) https://forum.flitsservice.nl/dashcam-info/dod-ls460w-gps-data-uit-mov-bestand-lezen-t87926.html
|
---|
11 | # 4) https://developers.google.com/streetview/publish/camm-spec
|
---|
12 | # 5) https://sergei.nz/extracting-gps-data-from-viofo-a119-and-other-novatek-powered-cameras/
|
---|
13 | # 6) Thomas Allen https://github.com/exiftool/exiftool/pull/62
|
---|
14 | #------------------------------------------------------------------------------
|
---|
15 | package Image::ExifTool::QuickTime;
|
---|
16 |
|
---|
17 | use strict;
|
---|
18 |
|
---|
19 | use Image::ExifTool qw(:DataAccess :Utils);
|
---|
20 | use Image::ExifTool::QuickTime;
|
---|
21 |
|
---|
22 | sub Process_tx3g($$$);
|
---|
23 | sub Process_marl($$$);
|
---|
24 | sub Process_mebx($$$);
|
---|
25 | sub ProcessFreeGPS($$$);
|
---|
26 | sub ProcessFreeGPS2($$$);
|
---|
27 | sub 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)
|
---|
31 | my %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
|
---|
62 | my @dateMax = ( 24, 59, 59, 2200, 12, 31 );
|
---|
63 |
|
---|
64 | # typical (minimum?) size of freeGPS block
|
---|
65 | my $gpsBlockSize = 0x8000;
|
---|
66 |
|
---|
67 | # conversion factors
|
---|
68 | my $knotsToKph = 1.852; # knots --> km/h
|
---|
69 | my $mpsToKph = 3.6; # m/s --> km/h
|
---|
70 |
|
---|
71 | # handler types to process based on MetaFormat/OtherFormat
|
---|
72 | my %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
|
---|
80 | my %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)
|
---|
89 | my %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)
|
---|
713 | sub 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
|
---|
797 | sub 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
|
---|
810 | sub 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
|
---|
840 | sub 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
|
---|
856 | sub 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)
|
---|
1099 | sub 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
|
---|
1332 | sub 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
|
---|
1589 | sub 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
|
---|
1626 | ATCRec: 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
|
---|
1888 | sub 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
|
---|
1990 | sub 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
|
---|
2010 | sub 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
|
---|
2031 | sub 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
|
---|
2074 | sub 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
|
---|
2102 | sub 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
|
---|
2148 | sub 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
|
---|
2177 | sub 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
|
---|
2271 | sub 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
|
---|
2314 | my %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 | );
|
---|
2323 | sub 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
|
---|
2431 | sub 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
|
---|
2564 | sub 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
|
---|
2593 | sub 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 |
|
---|
2672 | 1; # end
|
---|
2673 |
|
---|
2674 | __END__
|
---|
2675 |
|
---|
2676 | =head1 NAME
|
---|
2677 |
|
---|
2678 | Image::ExifTool::QuickTime - Extract embedded information from media data
|
---|
2679 |
|
---|
2680 | =head1 SYNOPSIS
|
---|
2681 |
|
---|
2682 | These routines are autoloaded by Image::ExifTool::QuickTime.
|
---|
2683 |
|
---|
2684 | =head1 DESCRIPTION
|
---|
2685 |
|
---|
2686 | This file contains routines used by Image::ExifTool to extract embedded
|
---|
2687 | information like GPS tracks from MOV, MP4 and INSV media data.
|
---|
2688 |
|
---|
2689 | =head1 AUTHOR
|
---|
2690 |
|
---|
2691 | Copyright 2003-2021, Phil Harvey (philharvey66 at gmail.com)
|
---|
2692 |
|
---|
2693 | This library is free software; you can redistribute it and/or modify it
|
---|
2694 | under 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 |
|
---|
2714 | L<Image::ExifTool::QuickTime(3pm)|Image::ExifTool::QuickTime>,
|
---|
2715 | L<Image::ExifTool::TagNames/QuickTime Stream Tags>,
|
---|
2716 | L<Image::ExifTool::TagNames/GoPro GPMF Tags>,
|
---|
2717 | L<Image::ExifTool::TagNames/Sony rtmd Tags>,
|
---|
2718 | L<Image::ExifTool(3pm)|Image::ExifTool>
|
---|
2719 |
|
---|
2720 | =cut
|
---|
2721 |
|
---|