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