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