1 | #------------------------------------------------------------------------------
|
---|
2 | # File: GoPro.pm
|
---|
3 | #
|
---|
4 | # Description: Read information from GoPro videos
|
---|
5 | #
|
---|
6 | # Revisions: 2018/01/12 - P. Harvey Created
|
---|
7 | #
|
---|
8 | # References: 1) https://github.com/gopro/gpmf-parser
|
---|
9 | # 2) https://github.com/stilldavid/gopro-utils
|
---|
10 | #------------------------------------------------------------------------------
|
---|
11 |
|
---|
12 | package Image::ExifTool::GoPro;
|
---|
13 |
|
---|
14 | use strict;
|
---|
15 | use vars qw($VERSION);
|
---|
16 | use Image::ExifTool qw(:DataAccess :Utils);
|
---|
17 | use Image::ExifTool::QuickTime;
|
---|
18 |
|
---|
19 | $VERSION = '1.06';
|
---|
20 |
|
---|
21 | sub ProcessGoPro($$$);
|
---|
22 | sub ProcessString($$$);
|
---|
23 | sub ScaleValues($$);
|
---|
24 | sub AddUnits($$$);
|
---|
25 | sub ConvertSystemTime($$);
|
---|
26 |
|
---|
27 | # GoPro data types that have ExifTool equivalents (ref 1)
|
---|
28 | my %goProFmt = ( # format codes
|
---|
29 | # 0x00 - container (subdirectory)
|
---|
30 | 0x62 => 'int8s', # 'b'
|
---|
31 | 0x42 => 'int8u', # 'B'
|
---|
32 | 0x63 => 'string', # 'c' (possibly null terminated)
|
---|
33 | 0x73 => 'int16s', # 's'
|
---|
34 | 0x53 => 'int16u', # 'S'
|
---|
35 | 0x6c => 'int32s', # 'l'
|
---|
36 | 0x4c => 'int32u', # 'L'
|
---|
37 | 0x66 => 'float', # 'f'
|
---|
38 | 0x64 => 'double', # 'd'
|
---|
39 | 0x46 => 'undef', # 'F' (4-char ID)
|
---|
40 | 0x47 => 'undef', # 'G' (16-byte uuid)
|
---|
41 | 0x6a => 'int64s', # 'j'
|
---|
42 | 0x4a => 'int64u', # 'J'
|
---|
43 | 0x71 => 'fixed32s', # 'q'
|
---|
44 | 0x51 => 'fixed64s', # 'Q'
|
---|
45 | 0x55 => 'undef', # 'U' (16-byte date)
|
---|
46 | 0x3f => 'undef', # '?' (complex structure)
|
---|
47 | );
|
---|
48 |
|
---|
49 | # sizes of format codes if different than what FormatSize() would return
|
---|
50 | my %goProSize = (
|
---|
51 | 0x46 => 4,
|
---|
52 | 0x47 => 16,
|
---|
53 | 0x55 => 16,
|
---|
54 | );
|
---|
55 |
|
---|
56 | # tagInfo elements to add units to PrintConv value
|
---|
57 | my %addUnits = (
|
---|
58 | AddUnits => 1,
|
---|
59 | PrintConv => 'Image::ExifTool::GoPro::AddUnits($self, $val, $tag)',
|
---|
60 | );
|
---|
61 |
|
---|
62 | # Tags found in the GPMF box of Hero6 mp4 videos (ref PH), and
|
---|
63 | # the gpmd-format timed metadata of Hero5 and Hero6 videos (ref 1)
|
---|
64 | %Image::ExifTool::GoPro::GPMF = (
|
---|
65 | PROCESS_PROC => \&ProcessGoPro,
|
---|
66 | GROUPS => { 2 => 'Camera' },
|
---|
67 | NOTES => q{
|
---|
68 | Tags extracted from the GPMF box of GoPro MP4 videos, the APP6 "GoPro"
|
---|
69 | segment of JPEG files, and from the "gpmd" timed metadata if the
|
---|
70 | L<ExtractEmbedded|../ExifTool.html#ExtractEmbedded> (-ee) option is enabled. Many more tags exist, but are
|
---|
71 | currently unknown and extracted only with the L<Unknown|../ExifTool.html#Unknown> (-u) option. Please
|
---|
72 | let me know if you discover the meaning of any of these unknown tags. See
|
---|
73 | L<https://github.com/gopro/gpmf-parser> for details about this format.
|
---|
74 | },
|
---|
75 | ACCL => { #2 (gpmd)
|
---|
76 | Name => 'Accelerometer',
|
---|
77 | Notes => 'accelerator readings in m/s2',
|
---|
78 | Binary => 1,
|
---|
79 | },
|
---|
80 | # ANGX (GPMF-GEOC) - seen -0.05 (fmt d, Max)
|
---|
81 | # ANGY (GPMF-GEOC) - seen 179.9 (fmt d, Max)
|
---|
82 | # ANGZ (GPMF-GEOC) - seen 0.152 (fmt d, Max)
|
---|
83 | ALLD => 'AutoLowLightDuration', #1 (gpmd) (untested)
|
---|
84 | # APTO (GPMF) - seen: 'RAW', 'DYNM' (fmt c)
|
---|
85 | ATTD => { #PH (Karma)
|
---|
86 | Name => 'Attitude',
|
---|
87 | # UNIT=s,rad,rad,rad,rad/s,rad/s,rad/s,
|
---|
88 | # TYPE=LffffffB
|
---|
89 | # SCAL=1000 1 1 1 1 1 1 1
|
---|
90 | Binary => 1,
|
---|
91 | },
|
---|
92 | ATTR => { #PH (Karma)
|
---|
93 | Name => 'AttitudeTarget',
|
---|
94 | # UNIT=s,rad,rad,rad,
|
---|
95 | # TYPE=Jffff
|
---|
96 | # SCAL=1000 1 1 1 1
|
---|
97 | Binary => 1,
|
---|
98 | },
|
---|
99 | AUDO => 'AudioSetting', #PH (GPMF - seen: 'WIND', fmt c)
|
---|
100 | # AUPT (GPMF) - seen: 'N','Y' (fmt c)
|
---|
101 | BPOS => { #PH (Karma)
|
---|
102 | Name => 'Controller',
|
---|
103 | Unknown => 1,
|
---|
104 | # UNIT=deg,deg,m,deg,deg,m,m,m
|
---|
105 | # TYPE=lllfffff
|
---|
106 | # SCAL=10000000 10000000 1000 1 1 1 1 1
|
---|
107 | %addUnits,
|
---|
108 | },
|
---|
109 | # BRID (GPMF) - seen: 0 (fmt B)
|
---|
110 | # BROD (GPMF) - seen: 'ASK','' (fmt c)
|
---|
111 | # CALH (GPMF-GEOC) - seen 3040 (fmt L, Max)
|
---|
112 | # CALW (GPMF-GEOC) - seen 4056 (fmt L, Max)
|
---|
113 | CASN => 'CameraSerialNumber', #PH (GPMF - seen: 'C3221324545448', fmt c)
|
---|
114 | # CINF (GPMF) - seen: 0x67376be7709bc8876a8baf3940908618, 0xe230988539b30cf5f016627ae8fc5395,
|
---|
115 | # 0x8bcbe424acc5b37d7d77001635198b3b (fmt B) (Camera INFormation?)
|
---|
116 | # CMOD (GPMF) - seen: 12,13,17 [12 360 video, 13 time-laps video, 17 JPEG] (fmt B)
|
---|
117 | # CRTX (GPMF-BACK/FRNT) - double[1]
|
---|
118 | # CRTY (GPMF-BACK/FRNT) - double[1]
|
---|
119 | CSEN => { #PH (Karma)
|
---|
120 | Name => 'CoyoteSense',
|
---|
121 | # UNIT=s,rad/s,rad/s,rad/s,g,g,g,,,,
|
---|
122 | # TYPE=LffffffLLLL
|
---|
123 | # SCAL=1000 1 1 1 1 1 1 1 1 1 1
|
---|
124 | Binary => 1,
|
---|
125 | },
|
---|
126 | CYTS => { #PH (Karma)
|
---|
127 | Name => 'CoyoteStatus',
|
---|
128 | # UNIT=s,,,,,rad,rad,rad,,
|
---|
129 | # TYPE=LLLLLfffBB
|
---|
130 | # SCAL=1000 1 1 1 1 1 1 1 1 1
|
---|
131 | Binary => 1,
|
---|
132 | },
|
---|
133 | DEVC => { #PH (gpmd,GPMF, fmt \0)
|
---|
134 | Name => 'DeviceContainer',
|
---|
135 | SubDirectory => { TagTable => 'Image::ExifTool::GoPro::GPMF' },
|
---|
136 | # (Max) DVID=1,DVNM='Global Settings',VERS,FMWR,LINF,CINF,CASN,MINF,MUID,CMOD,MTYP,OREN,
|
---|
137 | # DZOM,DZST,SMTR,PRTN,PTWB,PTSH,PTCL,EXPT,PIMX,PIMN,PTEV,RATE,SROT,ZFOV,VLTE,VLTA,
|
---|
138 | # EISE,EISA,AUPT,AUDO,BROD,BRID,PVUL,PRJT,SOFF
|
---|
139 | # (Max) DVID='GEOC',DVNM='Geometry Calibrations',SHFX,SHFY,SHFZ,ANGX,ANGY,ANGZ,CALW,CALH
|
---|
140 | # (Max) DVID='BACK',DVNM='Back Lens',KLNS,CTRX,CTRY,MFOV,SFTR
|
---|
141 | # (Max) DVID='FRNT',DVNM='Front Lens',KLNS,CTRX,CTRY,MFOV,SFTR
|
---|
142 | # (Max) DVID='HLMT',DVNM='Highlights'
|
---|
143 | },
|
---|
144 | # DVID (GPMF) - DeviceID; seen: 1 (fmt L), HLMT (fmt F), GEOC (fmt F), 'BACK' (fmt F, Max)
|
---|
145 | DVID => { Name => 'DeviceID', Unknown => 1 }, #2 (gpmd)
|
---|
146 | # DVNM (GPMF) seen: 'Video Global Settings' (fmt c), 'Highlights' (fmt c), 'Geometry Calibrations' (Max)
|
---|
147 | # DVNM (gpmd) seen: 'Camera' (Hero5), 'Hero6 Black' (Hero6), 'GoPro Karma v1.0' (Karma)
|
---|
148 | DVNM => 'DeviceName', #PH (n/c)
|
---|
149 | DZOM => { #PH (GPMF - seen: 'Y', fmt c)
|
---|
150 | Name => 'DigitalZoom',
|
---|
151 | PrintConv => { N => 'No', Y => 'Yes' },
|
---|
152 | },
|
---|
153 | # DZST (GPMF) - seen: 0 (fmt L) (something to do with digital zoom maybe?)
|
---|
154 | EISA => { #PH (GPMF) - seen: 'Y','N','HS EIS','N/A' (fmt c) [N was for a time-lapse video]
|
---|
155 | Name => 'ElectronicImageStabilization',
|
---|
156 | },
|
---|
157 | # EISE (GPMF) - seen: 'Y','N' (fmt c)
|
---|
158 | EMPT => { Name => 'Empty', Unknown => 1 }, #2 (gpmd)
|
---|
159 | ESCS => { #PH (Karma)
|
---|
160 | Name => 'EscapeStatus',
|
---|
161 | # UNIT=s,rpm,rpm,rpm,rpm,rpm,rpm,rpm,rpm,degC,degC,degC,degC,V,V,V,V,A,A,A,A,,,,,,,,,
|
---|
162 | # TYPE=JSSSSSSSSssssSSSSSSSSSSSSSSSSB
|
---|
163 | # (no SCAL!)
|
---|
164 | Unknown => 1,
|
---|
165 | %addUnits,
|
---|
166 | },
|
---|
167 | # EXPT (GPMF) - seen: '', 'AUTO' (fmt c)
|
---|
168 | FACE => 'FaceDetected', #PH (gpmd)
|
---|
169 | FCNM => 'FaceNumbers', #PH (gpmd) (faces counted per frame, ref 1)
|
---|
170 | FMWR => 'FirmwareVersion', #PH (GPMF - seen: HD6.01.01.51.00, fmt c)
|
---|
171 | FWVS => 'OtherFirmware', #PH (NC) (gpmd - seen: '1.1.11.0', Karma)
|
---|
172 | GLPI => { #PH (gpmd, Karma)
|
---|
173 | Name => 'GPSPos',
|
---|
174 | # UNIT=s,deg,deg,m,m,m/s,m/s,m/s,deg
|
---|
175 | # TYPE=LllllsssS
|
---|
176 | # SCAL=1000 10000000 10000000 1000 1000 100 100 100 100
|
---|
177 | RawConv => '$val', # necessary to use scaled value instead of raw data as subdir data
|
---|
178 | SubDirectory => { TagTable => 'Image::ExifTool::GoPro::GLPI' },
|
---|
179 | },
|
---|
180 | GPRI => { #PH (gpmd, Karma)
|
---|
181 | Name => 'GPSRaw',
|
---|
182 | # UNIT=s,deg,deg,m,m,m,m/s,deg,,
|
---|
183 | # TYPE=JlllSSSSBB
|
---|
184 | # SCAL=1000000,10000000,10000000,1000,100,100,100,100,1,1
|
---|
185 | Unknown => 1,
|
---|
186 | RawConv => '$val', # necessary to use scaled value instead of raw data as subdir data
|
---|
187 | SubDirectory => { TagTable => 'Image::ExifTool::GoPro::GPRI' },
|
---|
188 | },
|
---|
189 | GPS5 => { #2 (gpmd)
|
---|
190 | Name => 'GPSInfo',
|
---|
191 | # SCAL=10000000,10000000,1000,1000,100
|
---|
192 | RawConv => '$val', # necessary to use scaled value instead of raw data as subdir data
|
---|
193 | SubDirectory => { TagTable => 'Image::ExifTool::GoPro::GPS5' },
|
---|
194 | },
|
---|
195 | GPSF => { #2 (gpmd)
|
---|
196 | Name => 'GPSMeasureMode',
|
---|
197 | PrintConv => {
|
---|
198 | 2 => '2-Dimensional Measurement',
|
---|
199 | 3 => '3-Dimensional Measurement',
|
---|
200 | },
|
---|
201 | },
|
---|
202 | GPSP => { #2 (gpmd)
|
---|
203 | Name => 'GPSHPositioningError',
|
---|
204 | Description => 'GPS Horizontal Positioning Error',
|
---|
205 | ValueConv => '$val / 100', # convert from cm to m
|
---|
206 | },
|
---|
207 | GPSU => { #2 (gpmd)
|
---|
208 | Name => 'GPSDateTime',
|
---|
209 | Groups => { 2 => 'Time' },
|
---|
210 | # (HERO5 writes this in 'c' format, HERO6 writes 'U')
|
---|
211 | ValueConv => '$val =~ s/^(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})/20$1:$2:$3 $4:$5:/; $val',
|
---|
212 | PrintConv => '$self->ConvertDateTime($val)',
|
---|
213 | },
|
---|
214 | GYRO => { #2 (gpmd)
|
---|
215 | Name => 'Gyroscope',
|
---|
216 | Notes => 'gyroscope readings in rad/s',
|
---|
217 | Binary => 1,
|
---|
218 | },
|
---|
219 | # HFLG (APP6) - seen: 0
|
---|
220 | ISOE => 'ISOSpeeds', #PH (gpmd)
|
---|
221 | ISOG => { #2 (gpmd)
|
---|
222 | Name => 'ImageSensorGain',
|
---|
223 | Binary => 1,
|
---|
224 | },
|
---|
225 | KBAT => { #PH (gpmd) (Karma)
|
---|
226 | Name => 'BatteryStatus',
|
---|
227 | # UNIT=A,Ah,J,degC,V,V,V,V,s,%,,,,,%
|
---|
228 | # TYPE=lLlsSSSSSSSBBBb
|
---|
229 | # SCAL=1000,1000,0.00999999977648258,100,1000,1000,1000,1000,0.0166666675359011,1,1,1,1,1,1
|
---|
230 | RawConv => '$val', # necessary to use scaled value instead of raw data as subdir data
|
---|
231 | SubDirectory => { TagTable => 'Image::ExifTool::GoPro::KBAT' },
|
---|
232 | },
|
---|
233 | # KLNS (GPMF-BACK/FRNT) - double[5] (fmt d, Max)
|
---|
234 | # LINF (GPMF) - seen: LAJ7061916601668,C3341326002180,C33632245450981 (fmt c) (Lens INFormation?)
|
---|
235 | LNED => { #PH (Karma)
|
---|
236 | Name => 'LocalPositionNED',
|
---|
237 | # UNIT=s,m,m,m,m/s,m/s,m/s
|
---|
238 | # TYPE=Lffffff
|
---|
239 | # SCAL=1000 1 1 1 1 1 1
|
---|
240 | Binary => 1,
|
---|
241 | },
|
---|
242 | MAGN => 'Magnetometer', #1 (gpmd) (units of uT)
|
---|
243 | # MFOV (GPMF-BACK/FRNT) - seen: 100 (fmt d, Max)
|
---|
244 | MINF => { #PH (GPMF - seen: HERO6 Black, fmt c)
|
---|
245 | Name => 'Model',
|
---|
246 | Groups => { 2 => 'Camera' },
|
---|
247 | Description => 'Camera Model Name',
|
---|
248 | },
|
---|
249 | # MTYP (GPMF) - seen: 0,1,5,11 [1 for time-lapse video, 5 for 360 video, 11 for JPEG] (fmt B)
|
---|
250 | # MUID (GPMF) - seen: 3882563431 2278071152 967805802 411471936 0 0 0 0 (fmt L)
|
---|
251 | OREN => { #PH (GPMF - seen: 'U', fmt c)
|
---|
252 | Name => 'AutoRotation',
|
---|
253 | PrintConv => {
|
---|
254 | U => 'Up',
|
---|
255 | D => 'Down', # (NC)
|
---|
256 | A => 'Auto', # (NC)
|
---|
257 | },
|
---|
258 | },
|
---|
259 | # (most of the "P" tags are ProTune settings - PH)
|
---|
260 | PHDR => 'HDRSetting', #PH (APP6 - seen: 0)
|
---|
261 | PIMN => 'AutoISOMin', #PH (GPMF - seen: 100, fmt L)
|
---|
262 | PIMX => 'AutoISOMax', #PH (GPMF - seen: 1600, fmt L)
|
---|
263 | # PRAW (APP6) - seen: 0, 'N', 'Y' (fmt c)
|
---|
264 | PRES => 'PhotoResolution', #PH (APP6 - seen: '12MP_W')
|
---|
265 | # PRJT (APP6) - seen: 'GPRO','EACO' (fmt F, Hero8, Max)
|
---|
266 | PRTN => { #PH (GPMF - seen: 'N', fmt c)
|
---|
267 | Name => 'ProTune',
|
---|
268 | PrintConv => {
|
---|
269 | N => 'Off',
|
---|
270 | Y => 'On', # (NC)
|
---|
271 | },
|
---|
272 | },
|
---|
273 | PTCL => 'ColorMode', #PH (GPMF - seen: 'GOPRO', fmt c' APP6: 'FLAT')
|
---|
274 | PTEV => 'ExposureCompensation', #PH (GPMF - seen: '0.0', fmt c)
|
---|
275 | PTSH => 'Sharpness', #PH (GPMF - seen: 'HIGH', fmt c)
|
---|
276 | PTWB => 'WhiteBalance', #PH (GPMF - seen: 'AUTO', fmt c)
|
---|
277 | # PVUL (APP6) - seen: 'F' (fmt c, Hero8, Max)
|
---|
278 | RATE => 'Rate', #PH (GPMF - seen: '0_5SEC', fmt c; APP6 - seen: '4_1SEC')
|
---|
279 | RMRK => { #2 (gpmd)
|
---|
280 | Name => 'Comments',
|
---|
281 | ValueConv => '$self->Decode($val, "Latin")',
|
---|
282 | },
|
---|
283 | SCAL => { #2 (gpmd) scale factor for subsequent data
|
---|
284 | Name => 'ScaleFactor',
|
---|
285 | Unknown => 1,
|
---|
286 | },
|
---|
287 | SCPR => { #PH (Karma) [stream was empty]
|
---|
288 | Name => 'ScaledPressure',
|
---|
289 | # UNIT=s,Pa,Pa,degC
|
---|
290 | # TYPE=Lffs
|
---|
291 | # SCAL=1000 0.00999999977648258 0.00999999977648258 100
|
---|
292 | %addUnits,
|
---|
293 | },
|
---|
294 | # SFTR (GPMF-BACK/FRNT) - seen 0.999,1.00004 (fmt d, Max)
|
---|
295 | # SHFX (GPMF-GEOC) - seen 22.92 (fmt d, Max)
|
---|
296 | # SHFY (GPMF-GEOC) - seen 0.123 (fmt d, Max)
|
---|
297 | # SHFZ (GPMF-GEOC) - seen 36.06 (fmt d, Max)
|
---|
298 | SHUT => { #2 (gpmd)
|
---|
299 | Name => 'ExposureTimes',
|
---|
300 | PrintConv => q{
|
---|
301 | my @a = split ' ', $val;
|
---|
302 | $_ = Image::ExifTool::Exif::PrintExposureTime($_) foreach @a;
|
---|
303 | return join ' ', @a;
|
---|
304 | },
|
---|
305 | },
|
---|
306 | SIMU => { #PH (Karma)
|
---|
307 | Name => 'ScaledIMU',
|
---|
308 | # UNIT=s,g,g,g,rad/s,rad/s,rad/s,T,T,T
|
---|
309 | # TYPE=Lsssssssss
|
---|
310 | # SCAL=1000 1000 1000 1000 1000 1000 1000 1000 1000 1000
|
---|
311 | %addUnits,
|
---|
312 | },
|
---|
313 | SIUN => { #2 (gpmd - seen : 'm/s2','rad/s')
|
---|
314 | Name => 'SIUnits',
|
---|
315 | Unknown => 1,
|
---|
316 | ValueConv => '$self->Decode($val, "Latin")',
|
---|
317 | },
|
---|
318 | # SMTR (GPMF) - seen: 'N' (fmt c)
|
---|
319 | # SOFF (APP6) - seen: 0 (fmt L, Hero8, Max)
|
---|
320 | # SROT (GPMF) - seen 20.60 (fmt f, Max)
|
---|
321 | STMP => { #1 (gpmd)
|
---|
322 | Name => 'TimeStamp',
|
---|
323 | ValueConv => '$val / 1e6',
|
---|
324 | },
|
---|
325 | STRM => { #2 (gpmd,GPMF, fmt \0)
|
---|
326 | Name => 'NestedSignalStream',
|
---|
327 | SubDirectory => { TagTable => 'Image::ExifTool::GoPro::GPMF' },
|
---|
328 | },
|
---|
329 | STNM => { #2 (gpmd)
|
---|
330 | Name => 'StreamName',
|
---|
331 | Unknown => 1,
|
---|
332 | ValueConv => '$self->Decode($val, "Latin")',
|
---|
333 | },
|
---|
334 | SYST => { #PH (Karma)
|
---|
335 | Name => 'SystemTime',
|
---|
336 | # UNIT=s,s
|
---|
337 | # TYPE=JJ
|
---|
338 | # SCAL=1000000 1000
|
---|
339 | # save system time calibrations for later
|
---|
340 | RawConv => q{
|
---|
341 | my @v = split ' ', $val;
|
---|
342 | if (@v == 2) {
|
---|
343 | my $s = $$self{SystemTimeList};
|
---|
344 | $s or $s = $$self{SystemTimeList} = [ ];
|
---|
345 | push @$s, \@v;
|
---|
346 | }
|
---|
347 | return $val;
|
---|
348 | },
|
---|
349 | },
|
---|
350 | # TICK => { Name => 'InTime', Unknown => 1, ValueConv => '$val/1000' }, #1 (gpmd)
|
---|
351 | TMPC => { #2 (gpmd)
|
---|
352 | Name => 'CameraTemperature',
|
---|
353 | PrintConv => '"$val C"',
|
---|
354 | },
|
---|
355 | # TOCK => { Name => 'OutTime', Unknown => 1, ValueConv => '$val/1000' }, #1 (gpmd)
|
---|
356 | TSMP => { Name => 'TotalSamples', Unknown => 1 }, #2 (gpmd)
|
---|
357 | TYPE => { Name => 'StructureType', Unknown => 1 }, #2 (gpmd,GPMF - eg 'LLLllfFff', fmt c)
|
---|
358 | UNIT => { #2 (gpmd) alternative units
|
---|
359 | Name => 'Units',
|
---|
360 | Unknown => 1,
|
---|
361 | ValueConv => '$self->Decode($val, "Latin")',
|
---|
362 | },
|
---|
363 | VERS => {
|
---|
364 | Name => 'MetadataVersion',
|
---|
365 | PrintConv => '$val =~ tr/ /./; $val',
|
---|
366 | },
|
---|
367 | VFOV => { #PH (GPMF - seen: 'W', fmt c)
|
---|
368 | Name => 'FieldOfView',
|
---|
369 | PrintConv => {
|
---|
370 | W => 'Wide',
|
---|
371 | S => 'Super View', # (NC, not seen)
|
---|
372 | L => 'Linear', # (NC, not seen)
|
---|
373 | },
|
---|
374 | },
|
---|
375 | # VLTA (GPMF) - seen: 78 ('N') (fmt B -- wrong format?)
|
---|
376 | VFRH => { #PH (Karma)
|
---|
377 | Name => 'VisualFlightRulesHUD',
|
---|
378 | BinaryData => 1,
|
---|
379 | # UNIT=m/s,m/s,m,m/s,deg,%
|
---|
380 | # TYPE=ffffsS
|
---|
381 | },
|
---|
382 | # VLTE (GPMF) - seen: 'Y','N' (fmt c)
|
---|
383 | WBAL => 'ColorTemperatures', #PH (gpmd)
|
---|
384 | WRGB => { #PH (gpmd)
|
---|
385 | Name => 'WhiteBalanceRGB',
|
---|
386 | Binary => 1,
|
---|
387 | },
|
---|
388 | # ZFOV (APP6,GPMF) - seen: 148.34, 0 (fmt f, Hero8, Max)
|
---|
389 | );
|
---|
390 |
|
---|
391 | # GoPro GPS5 tags (ref 2) (Hero5,Hero6)
|
---|
392 | %Image::ExifTool::GoPro::GPS5 = (
|
---|
393 | PROCESS_PROC => \&ProcessString,
|
---|
394 | GROUPS => { 1 => 'GoPro', 2 => 'Location' },
|
---|
395 | VARS => { HEX_ID => 0, ID_LABEL => 'Index' },
|
---|
396 | 0 => { # (unit='deg')
|
---|
397 | Name => 'GPSLatitude',
|
---|
398 | PrintConv => 'Image::ExifTool::GPS::ToDMS($self, $val, 1, "N")',
|
---|
399 | },
|
---|
400 | 1 => { # (unit='deg')
|
---|
401 | Name => 'GPSLongitude',
|
---|
402 | PrintConv => 'Image::ExifTool::GPS::ToDMS($self, $val, 1, "E")',
|
---|
403 | },
|
---|
404 | 2 => { # (unit='m')
|
---|
405 | Name => 'GPSAltitude',
|
---|
406 | PrintConv => '"$val m"',
|
---|
407 | },
|
---|
408 | 3 => 'GPSSpeed', # (unit='m/s')
|
---|
409 | 4 => 'GPSSpeed3D', # (unit='m/s')
|
---|
410 | );
|
---|
411 |
|
---|
412 | # GoPro GPRI tags (ref PH) (Karma)
|
---|
413 | %Image::ExifTool::GoPro::GPRI = (
|
---|
414 | PROCESS_PROC => \&ProcessString,
|
---|
415 | GROUPS => { 1 => 'GoPro', 2 => 'Location' },
|
---|
416 | VARS => { HEX_ID => 0, ID_LABEL => 'Index' },
|
---|
417 | 0 => { # (unit='s')
|
---|
418 | Name => 'GPSDateTimeRaw',
|
---|
419 | Groups => { 2 => 'Time' },
|
---|
420 | ValueConv => \&ConvertSystemTime, # convert to date/time based on SystemTime clock
|
---|
421 | PrintConv => '$self->ConvertDateTime($val)',
|
---|
422 | },
|
---|
423 | 1 => { # (unit='deg')
|
---|
424 | Name => 'GPSLatitudeRaw',
|
---|
425 | PrintConv => 'Image::ExifTool::GPS::ToDMS($self, $val, 1, "N")',
|
---|
426 | },
|
---|
427 | 2 => { # (unit='deg')
|
---|
428 | Name => 'GPSLongitudeRaw',
|
---|
429 | PrintConv => 'Image::ExifTool::GPS::ToDMS($self, $val, 1, "E")',
|
---|
430 | },
|
---|
431 | 3 => {
|
---|
432 | Name => 'GPSAltitudeRaw', # (NC)
|
---|
433 | PrintConv => '"$val m"',
|
---|
434 | },
|
---|
435 | # (unknown tags must be defined so that ProcessString() will iterate through all values)
|
---|
436 | 4 => { Name => 'GPRI_Unknown4', Unknown => 1, Hidden => 1, PrintConv => '"$val m"' },
|
---|
437 | 5 => { Name => 'GPRI_Unknown5', Unknown => 1, Hidden => 1, PrintConv => '"$val m"' },
|
---|
438 | 6 => 'GPSSpeedRaw', # (NC) # (unit='m/s' -- should convert to other units?)
|
---|
439 | 7 => 'GPSTrackRaw', # (NC) # (unit='deg')
|
---|
440 | 8 => { Name => 'GPRI_Unknown8', Unknown => 1, Hidden => 1 }, # (no units)
|
---|
441 | 9 => { Name => 'GPRI_Unknown9', Unknown => 1, Hidden => 1 }, # (no units)
|
---|
442 | );
|
---|
443 |
|
---|
444 | # GoPro GLPI tags (ref PH) (Karma)
|
---|
445 | %Image::ExifTool::GoPro::GLPI = (
|
---|
446 | PROCESS_PROC => \&ProcessString,
|
---|
447 | GROUPS => { 1 => 'GoPro', 2 => 'Location' },
|
---|
448 | VARS => { HEX_ID => 0, ID_LABEL => 'Index' },
|
---|
449 | 0 => { # (unit='s')
|
---|
450 | Name => 'GPSDateTime',
|
---|
451 | Groups => { 2 => 'Time' },
|
---|
452 | ValueConv => \&ConvertSystemTime, # convert to date/time based on SystemTime clock
|
---|
453 | PrintConv => '$self->ConvertDateTime($val)',
|
---|
454 | },
|
---|
455 | 1 => { # (unit='deg')
|
---|
456 | Name => 'GPSLatitude',
|
---|
457 | PrintConv => 'Image::ExifTool::GPS::ToDMS($self, $val, 1, "N")',
|
---|
458 | },
|
---|
459 | 2 => { # (unit='deg')
|
---|
460 | Name => 'GPSLongitude',
|
---|
461 | PrintConv => 'Image::ExifTool::GPS::ToDMS($self, $val, 1, "E")',
|
---|
462 | },
|
---|
463 | 3 => { # (unit='m')
|
---|
464 | Name => 'GPSAltitude', # (NC)
|
---|
465 | PrintConv => '"$val m"',
|
---|
466 | },
|
---|
467 | # (unknown tags must be defined so that ProcessString() will iterate through all values)
|
---|
468 | 4 => { Name => 'GLPI_Unknown4', Unknown => 1, Hidden => 1, PrintConv => '"$val m"' },
|
---|
469 | 5 => { Name => 'GPSSpeedX', PrintConv => '"$val m/s"' }, # (NC)
|
---|
470 | 6 => { Name => 'GPSSpeedY', PrintConv => '"$val m/s"' }, # (NC)
|
---|
471 | 7 => { Name => 'GPSSpeedZ', PrintConv => '"$val m/s"' }, # (NC)
|
---|
472 | 8 => { Name => 'GPSTrack' }, # (unit='deg')
|
---|
473 | );
|
---|
474 |
|
---|
475 | # GoPro KBAT tags (ref PH)
|
---|
476 | %Image::ExifTool::GoPro::KBAT = (
|
---|
477 | PROCESS_PROC => \&ProcessString,
|
---|
478 | GROUPS => { 1 => 'GoPro', 2 => 'Camera' },
|
---|
479 | VARS => { HEX_ID => 0, ID_LABEL => 'Index' },
|
---|
480 | NOTES => 'Battery status information found in GoPro Karma videos.',
|
---|
481 | 0 => { Name => 'BatteryCurrent', PrintConv => '"$val A"' },
|
---|
482 | 1 => { Name => 'BatteryCapacity', PrintConv => '"$val Ah"' },
|
---|
483 | 2 => { Name => 'KBAT_Unknown2', PrintConv => '"$val J"', Unknown => 1, Hidden => 1 },
|
---|
484 | 3 => { Name => 'BatteryTemperature', PrintConv => '"$val C"' },
|
---|
485 | 4 => { Name => 'BatteryVoltage1', PrintConv => '"$val V"' },
|
---|
486 | 5 => { Name => 'BatteryVoltage2', PrintConv => '"$val V"' },
|
---|
487 | 6 => { Name => 'BatteryVoltage3', PrintConv => '"$val V"' },
|
---|
488 | 7 => { Name => 'BatteryVoltage4', PrintConv => '"$val V"' },
|
---|
489 | 8 => { Name => 'BatteryTime', PrintConv => 'ConvertDuration(int($val + 0.5))' }, # (NC)
|
---|
490 | 9 => { Name => 'KBAT_Unknown9', PrintConv => '"$val %"', Unknown => 1, Hidden => 1, },
|
---|
491 | 10 => { Name => 'KBAT_Unknown10', Unknown => 1, Hidden => 1 }, # (no units)
|
---|
492 | 11 => { Name => 'KBAT_Unknown11', Unknown => 1, Hidden => 1 }, # (no units)
|
---|
493 | 12 => { Name => 'KBAT_Unknown12', Unknown => 1, Hidden => 1 }, # (no units)
|
---|
494 | 13 => { Name => 'KBAT_Unknown13', Unknown => 1, Hidden => 1 }, # (no units)
|
---|
495 | 14 => { Name => 'BatteryLevel', PrintConv => '"$val %"' },
|
---|
496 | );
|
---|
497 |
|
---|
498 | # GoPro fdsc tags written by the Hero5 and Hero6 (ref PH)
|
---|
499 | %Image::ExifTool::GoPro::fdsc = (
|
---|
500 | GROUPS => { 2 => 'Camera' },
|
---|
501 | PROCESS_PROC => \&Image::ExifTool::ProcessBinaryData,
|
---|
502 | NOTES => q{
|
---|
503 | Tags extracted from the MP4 "fdsc" timed metadata when the L<ExtractEmbedded|../ExifTool.html#ExtractEmbedded>
|
---|
504 | (-ee) option is used.
|
---|
505 | },
|
---|
506 | 0x08 => { Name => 'FirmwareVersion', Format => 'string[15]' },
|
---|
507 | 0x17 => { Name => 'SerialNumber', Format => 'string[16]' },
|
---|
508 | 0x57 => { Name => 'OtherSerialNumber', Format => 'string[15]' }, # (NC)
|
---|
509 | 0x66 => {
|
---|
510 | Name => 'Model',
|
---|
511 | Description => 'Camera Model Name',
|
---|
512 | Format => 'string[16]',
|
---|
513 | },
|
---|
514 | # ...
|
---|
515 | # after this there are lots of interesting values also found in the GPMF box,
|
---|
516 | # but this block is lacking tag ID's and any directory structure, so the
|
---|
517 | # value offsets are therefore presumably firmware dependent :(
|
---|
518 | );
|
---|
519 |
|
---|
520 | #------------------------------------------------------------------------------
|
---|
521 | # Convert system time to date/time string
|
---|
522 | # Inputs: 0) system time value, 1) ExifTool ref
|
---|
523 | # Returns: EXIF-format date/time string with milliseconds
|
---|
524 | sub ConvertSystemTime($$)
|
---|
525 | {
|
---|
526 | my ($val, $et) = @_;
|
---|
527 | my $s = $$et{SystemTimeList} or return '<uncalibrated>';
|
---|
528 | unless ($$et{SystemTimeListSorted}) {
|
---|
529 | $s = $$et{SystemTimeList} = [ sort { $$a[0] <=> $$b[0] } @$s ];
|
---|
530 | $$et{SystemTimeListSorted} = 1;
|
---|
531 | }
|
---|
532 | my ($i, $j) = (0, $#$s);
|
---|
533 | # perform binary search to find this system time value
|
---|
534 | while ($j - $i > 1) {
|
---|
535 | my $t = int(($i + $j) / 2);
|
---|
536 | ($val < $$s[$t][0] ? $j : $i) = $t;
|
---|
537 | }
|
---|
538 | if ($i == $j or $$s[$j][0] == $$s[$i][0]) {
|
---|
539 | $val = $$s[$i][1];
|
---|
540 | } else {
|
---|
541 | # interpolate between values
|
---|
542 | $val = $$s[$i][1] + ($$s[$j][1] - $$s[$i][1]) * ($val - $$s[$i][0]) / ($$s[$j][0] - $$s[$i][0]);
|
---|
543 | }
|
---|
544 | # (a bit tricky to remove fractional seconds then add them back again after
|
---|
545 | # the date/time conversion while avoiding round-off errors which could
|
---|
546 | # put the seconds out by 1...)
|
---|
547 | my ($t, $f) = ("$val" =~ /^(\d+)(\.\d+)/);
|
---|
548 | return Image::ExifTool::ConvertUnixTime($t, $$et{OPTIONS}{QuickTimeUTC}) . $f;
|
---|
549 | }
|
---|
550 |
|
---|
551 | #------------------------------------------------------------------------------
|
---|
552 | # Scale values by last 'SCAL' constants
|
---|
553 | # Inputs: 0) value or list of values, 1) string of scale factors
|
---|
554 | # Returns: nothing, but updates values
|
---|
555 | sub ScaleValues($$)
|
---|
556 | {
|
---|
557 | my ($val, $scl) = @_;
|
---|
558 | return unless $val and $scl;
|
---|
559 | my @scl = split ' ', $scl or return;
|
---|
560 | my @scaled;
|
---|
561 | my $v = (ref $val eq 'ARRAY') ? $val : [ $val ];
|
---|
562 | foreach $val (@$v) {
|
---|
563 | my @a = split ' ', $val;
|
---|
564 | $a[$_] /= $scl[$_ % @scl] foreach 0..$#a;
|
---|
565 | push @scaled, join(' ', @a);
|
---|
566 | }
|
---|
567 | $_[0] = @scaled > 1 ? \@scaled : $scaled[0];
|
---|
568 | }
|
---|
569 |
|
---|
570 | #------------------------------------------------------------------------------
|
---|
571 | # Add units to values for human-readable output
|
---|
572 | # Inputs: 0) ExifTool ref, 1) value, 2) tag key
|
---|
573 | # Returns: converted value
|
---|
574 | sub AddUnits($$$)
|
---|
575 | {
|
---|
576 | my ($et, $val, $tag) = @_;
|
---|
577 | if ($et and $$et{TAG_EXTRA}{$tag} and $$et{TAG_EXTRA}{$tag}{Units}) {
|
---|
578 | my $u = $$et{TAG_EXTRA}{$tag}{Units};
|
---|
579 | $u = [ $u ] unless ref $u eq 'ARRAY';
|
---|
580 | my @a = split ' ', $val;
|
---|
581 | if (@$u == @a) {
|
---|
582 | my $i;
|
---|
583 | for ($i=0; $i<@a; ++$i) {
|
---|
584 | $a[$i] .= ' ' . $$u[$i] if $$u[$i];
|
---|
585 | }
|
---|
586 | $val = join ' ', @a;
|
---|
587 | }
|
---|
588 | }
|
---|
589 | return $val;
|
---|
590 | }
|
---|
591 |
|
---|
592 | #------------------------------------------------------------------------------
|
---|
593 | # Process string of values (or array of strings) to extract as separate tags
|
---|
594 | # Inputs: 0) ExifTool object ref, 1) directory information ref, 2) tag table ref
|
---|
595 | # Returns: 1 on success
|
---|
596 | sub ProcessString($$$)
|
---|
597 | {
|
---|
598 | my ($et, $dirInfo, $tagTablePtr) = @_;
|
---|
599 | my $dataPt = $$dirInfo{DataPt};
|
---|
600 | my @list = ref $$dataPt eq 'ARRAY' ? @{$$dataPt} : ( $$dataPt );
|
---|
601 | my ($string, $val);
|
---|
602 | $et->VerboseDir('GoPro structure');
|
---|
603 | foreach $string (@list) {
|
---|
604 | my @val = split ' ', $string;
|
---|
605 | my $i = 0;
|
---|
606 | foreach $val (@val) {
|
---|
607 | $et->HandleTag($tagTablePtr, $i, $val);
|
---|
608 | $$tagTablePtr{++$i} or $i = 0;
|
---|
609 | }
|
---|
610 | }
|
---|
611 | return 1;
|
---|
612 | }
|
---|
613 |
|
---|
614 | #------------------------------------------------------------------------------
|
---|
615 | # Process GoPro metadata (gpmd samples, GPMF box, or APP6) (ref PH/1/2)
|
---|
616 | # Inputs: 0) ExifTool object ref, 1) dirInfo ref, 2) tag table ref
|
---|
617 | # Returns: 1 on success
|
---|
618 | # - with hack to check for encrypted text in gpmd data (Rove Stealth 4K)
|
---|
619 | sub ProcessGoPro($$$)
|
---|
620 | {
|
---|
621 | my ($et, $dirInfo, $tagTablePtr) = @_;
|
---|
622 | my $dataPt = $$dirInfo{DataPt};
|
---|
623 | my $base = $$dirInfo{Base};
|
---|
624 | my $pos = $$dirInfo{DirStart} || 0;
|
---|
625 | my $dirEnd = $pos + ($$dirInfo{DirLen} || (length($$dataPt) - $pos));
|
---|
626 | my $verbose = $et->Options('Verbose');
|
---|
627 | my $unknown = $verbose || $et->Options('Unknown');
|
---|
628 | my ($size, $type, $unit, $scal, $setGroup0);
|
---|
629 |
|
---|
630 | $et->VerboseDir($$dirInfo{DirName} || 'GPMF', undef, $dirEnd-$pos) if $verbose;
|
---|
631 | if ($pos) {
|
---|
632 | my $parent = $$dirInfo{Parent};
|
---|
633 | $setGroup0 = $$et{SET_GROUP0} = 'APP6' if $parent and $parent eq 'APP6';
|
---|
634 | } else {
|
---|
635 | # set group0 to "QuickTime" unless group1 is being changed (to Track#)
|
---|
636 | $setGroup0 = $$et{SET_GROUP0} = 'QuickTime' unless $$et{SET_GROUP1};
|
---|
637 | }
|
---|
638 |
|
---|
639 | for (; $pos+8<=$dirEnd; $pos+=($size+3)&0xfffffffc) {
|
---|
640 | my ($tag,$fmt,$len,$count) = unpack("x${pos}a4CCn", $$dataPt);
|
---|
641 | $size = $len * $count;
|
---|
642 | $pos += 8;
|
---|
643 | last if $pos + $size > $dirEnd;
|
---|
644 | my $tagInfo = $et->GetTagInfo($tagTablePtr, $tag);
|
---|
645 | last if $tag eq "\0\0\0\0"; # stop at null tag
|
---|
646 | next unless $size or $verbose; # don't save empty values unless verbose
|
---|
647 | my $format = $goProFmt{$fmt} || 'undef';
|
---|
648 | my ($val, $i, $j, $p, @v);
|
---|
649 | if ($fmt == 0x3f and defined $type) {
|
---|
650 | # decode structure with format given by previous 'TYPE'
|
---|
651 | for ($i=0; $i<$count; ++$i) {
|
---|
652 | my (@s, $l);
|
---|
653 | for ($j=0, $p=0; $j<length($type); ++$j, $p+=$l) {
|
---|
654 | my $b = Get8u(\$type, $j);
|
---|
655 | my $f = $goProFmt{$b} or last;
|
---|
656 | $l = $goProSize{$b} || Image::ExifTool::FormatSize($f) or last;
|
---|
657 | last if $p + $l > $len;
|
---|
658 | my $s = ReadValue($dataPt, $pos+$i*$len+$p, $f, undef, $l);
|
---|
659 | last unless defined $s;
|
---|
660 | push @s, $s;
|
---|
661 | }
|
---|
662 | push @v, join ' ', @s if @s;
|
---|
663 | }
|
---|
664 | $val = @v > 1 ? \@v : $v[0];
|
---|
665 | } elsif (($format eq 'undef' or $format eq 'string') and $count > 1 and $len > 1) {
|
---|
666 | # unpack multiple undef/string values as a list
|
---|
667 | my $a = $format eq 'undef' ? 'a' : 'A';
|
---|
668 | $val = [ unpack("x${pos}".("$a$len" x $count), $$dataPt) ];
|
---|
669 | } else {
|
---|
670 | $val = ReadValue($dataPt, $pos, $format, undef, $size);
|
---|
671 | }
|
---|
672 | # save TYPE, UNIT/SIUN and SCAL values for later
|
---|
673 | $type = $val if $tag eq 'TYPE';
|
---|
674 | $unit = $val if $tag eq 'UNIT' or $tag eq 'SIUN';
|
---|
675 | $scal = $val if $tag eq 'SCAL';
|
---|
676 |
|
---|
677 | unless ($tagInfo) {
|
---|
678 | next unless $unknown;
|
---|
679 | my $name = Image::ExifTool::QuickTime::PrintableTagID($tag);
|
---|
680 | $tagInfo = { Name => "GoPro_$name", Description => "GoPro $name", Unknown => 1 };
|
---|
681 | $$tagInfo{SubDirectory} = { TagTable => 'Image::ExifTool::GoPro::GPMF' } if not $fmt;
|
---|
682 | AddTagToTable($tagTablePtr, $tag, $tagInfo);
|
---|
683 | }
|
---|
684 | # apply scaling if available to last tag in this container
|
---|
685 | ScaleValues($val, $scal) if $scal and $tag ne 'SCAL' and $pos+$size+3>=$dirEnd;
|
---|
686 | my $key = $et->HandleTag($tagTablePtr, $tag, $val,
|
---|
687 | DataPt => $dataPt,
|
---|
688 | Base => $base,
|
---|
689 | Start => $pos,
|
---|
690 | Size => $size,
|
---|
691 | TagInfo => $tagInfo,
|
---|
692 | Format => $format,
|
---|
693 | Extra => $verbose ? ", type='".($fmt ? chr($fmt) : '\0')."' size=$len count=$count" : undef,
|
---|
694 | );
|
---|
695 | # save units for adding in print conversion if specified
|
---|
696 | $$et{TAG_EXTRA}{$key}{Units} = $unit if $$tagInfo{AddUnits} and $key;
|
---|
697 | }
|
---|
698 | delete $$et{SET_GROUP0} if $setGroup0;
|
---|
699 | return 1;
|
---|
700 | }
|
---|
701 |
|
---|
702 | 1; # end
|
---|
703 |
|
---|
704 | __END__
|
---|
705 |
|
---|
706 | =head1 NAME
|
---|
707 |
|
---|
708 | Image::ExifTool::GoPro - Read information from GoPro videos
|
---|
709 |
|
---|
710 | =head1 SYNOPSIS
|
---|
711 |
|
---|
712 | This module is used by Image::ExifTool
|
---|
713 |
|
---|
714 | =head1 DESCRIPTION
|
---|
715 |
|
---|
716 | This module contains definitions required by Image::ExifTool to decode
|
---|
717 | metadata from GoPro MP4 videos.
|
---|
718 |
|
---|
719 | =head1 AUTHOR
|
---|
720 |
|
---|
721 | Copyright 2003-2021, Phil Harvey (philharvey66 at gmail.com)
|
---|
722 |
|
---|
723 | This library is free software; you can redistribute it and/or modify it
|
---|
724 | under the same terms as Perl itself.
|
---|
725 |
|
---|
726 | =head1 REFERENCES
|
---|
727 |
|
---|
728 | =over 4
|
---|
729 |
|
---|
730 | =item L<https://github.com/gopro/gpmf-parser>
|
---|
731 |
|
---|
732 | =item L<https://github.com/stilldavid/gopro-utils>
|
---|
733 |
|
---|
734 | =back
|
---|
735 |
|
---|
736 | =head1 SEE ALSO
|
---|
737 |
|
---|
738 | L<Image::ExifTool::TagNames/GoPro Tags>,
|
---|
739 | L<Image::ExifTool(3pm)|Image::ExifTool>
|
---|
740 |
|
---|
741 | =cut
|
---|
742 |
|
---|