exiftool_vendored 10.65.0 → 11.41.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of exiftool_vendored might be problematic. Click here for more details.

Files changed (205) hide show
  1. checksums.yaml +4 -4
  2. data/bin/Changes +818 -19
  3. data/bin/MANIFEST +38 -0
  4. data/bin/META.json +1 -1
  5. data/bin/META.yml +1 -1
  6. data/bin/README +48 -44
  7. data/bin/arg_files/exif2xmp.args +4 -1
  8. data/bin/arg_files/gps2xmp.args +4 -1
  9. data/bin/arg_files/iptcCore.args +8 -0
  10. data/bin/arg_files/xmp2exif.args +4 -1
  11. data/bin/arg_files/xmp2gps.args +4 -1
  12. data/bin/config_files/dji.config +131 -0
  13. data/bin/config_files/example.config +6 -2
  14. data/bin/config_files/gps2utm.config +256 -256
  15. data/bin/config_files/nksc.config +146 -0
  16. data/bin/config_files/picasa_faces.config +382 -382
  17. data/bin/exiftool +688 -408
  18. data/bin/fmt_files/gpx.fmt +10 -6
  19. data/bin/fmt_files/gpx_wpt.fmt +10 -6
  20. data/bin/fmt_files/kml.fmt +8 -5
  21. data/bin/lib/File/RandomAccess.pm +48 -8
  22. data/bin/lib/File/RandomAccess.pod +21 -2
  23. data/bin/lib/Image/ExifTool.pm +645 -256
  24. data/bin/lib/Image/ExifTool.pod +219 -164
  25. data/bin/lib/Image/ExifTool/AES.pm +1 -1
  26. data/bin/lib/Image/ExifTool/AFCP.pm +3 -8
  27. data/bin/lib/Image/ExifTool/AIFF.pm +12 -4
  28. data/bin/lib/Image/ExifTool/APE.pm +1 -1
  29. data/bin/lib/Image/ExifTool/APP12.pm +1 -1
  30. data/bin/lib/Image/ExifTool/ASF.pm +19 -6
  31. data/bin/lib/Image/ExifTool/Apple.pm +13 -5
  32. data/bin/lib/Image/ExifTool/Audible.pm +1 -1
  33. data/bin/lib/Image/ExifTool/BMP.pm +1 -1
  34. data/bin/lib/Image/ExifTool/BPG.pm +17 -15
  35. data/bin/lib/Image/ExifTool/BZZ.pm +1 -1
  36. data/bin/lib/Image/ExifTool/BigTIFF.pm +30 -15
  37. data/bin/lib/Image/ExifTool/BuildTagLookup.pm +103 -52
  38. data/bin/lib/Image/ExifTool/Canon.pm +684 -112
  39. data/bin/lib/Image/ExifTool/CanonCustom.pm +119 -9
  40. data/bin/lib/Image/ExifTool/CanonRaw.pm +1 -1
  41. data/bin/lib/Image/ExifTool/CanonVRD.pm +13 -26
  42. data/bin/lib/Image/ExifTool/CaptureOne.pm +1 -1
  43. data/bin/lib/Image/ExifTool/Casio.pm +1 -1
  44. data/bin/lib/Image/ExifTool/Charset.pm +1 -1
  45. data/bin/lib/Image/ExifTool/DICOM.pm +12 -5
  46. data/bin/lib/Image/ExifTool/DJI.pm +51 -3
  47. data/bin/lib/Image/ExifTool/DNG.pm +15 -8
  48. data/bin/lib/Image/ExifTool/DPX.pm +1 -1
  49. data/bin/lib/Image/ExifTool/DV.pm +1 -1
  50. data/bin/lib/Image/ExifTool/DarwinCore.pm +63 -23
  51. data/bin/lib/Image/ExifTool/DjVu.pm +4 -2
  52. data/bin/lib/Image/ExifTool/EXE.pm +30 -6
  53. data/bin/lib/Image/ExifTool/Exif.pm +351 -109
  54. data/bin/lib/Image/ExifTool/FITS.pm +148 -0
  55. data/bin/lib/Image/ExifTool/FLAC.pm +2 -2
  56. data/bin/lib/Image/ExifTool/FLIF.pm +1 -1
  57. data/bin/lib/Image/ExifTool/FLIR.pm +109 -13
  58. data/bin/lib/Image/ExifTool/Fixup.pm +1 -1
  59. data/bin/lib/Image/ExifTool/Flash.pm +3 -3
  60. data/bin/lib/Image/ExifTool/FlashPix.pm +433 -9
  61. data/bin/lib/Image/ExifTool/Font.pm +2 -2
  62. data/bin/lib/Image/ExifTool/FotoStation.pm +1 -1
  63. data/bin/lib/Image/ExifTool/FujiFilm.pm +336 -16
  64. data/bin/lib/Image/ExifTool/GE.pm +1 -1
  65. data/bin/lib/Image/ExifTool/GIF.pm +5 -7
  66. data/bin/lib/Image/ExifTool/GIMP.pm +39 -3
  67. data/bin/lib/Image/ExifTool/GPS.pm +48 -22
  68. data/bin/lib/Image/ExifTool/GeoTiff.pm +23 -23
  69. data/bin/lib/Image/ExifTool/Geotag.pm +80 -45
  70. data/bin/lib/Image/ExifTool/GoPro.pm +709 -0
  71. data/bin/lib/Image/ExifTool/H264.pm +40 -18
  72. data/bin/lib/Image/ExifTool/HP.pm +1 -1
  73. data/bin/lib/Image/ExifTool/HTML.pm +19 -12
  74. data/bin/lib/Image/ExifTool/HtmlDump.pm +37 -26
  75. data/bin/lib/Image/ExifTool/ICC_Profile.pm +297 -23
  76. data/bin/lib/Image/ExifTool/ID3.pm +12 -7
  77. data/bin/lib/Image/ExifTool/IPTC.pm +48 -19
  78. data/bin/lib/Image/ExifTool/ISO.pm +1 -1
  79. data/bin/lib/Image/ExifTool/ITC.pm +1 -1
  80. data/bin/lib/Image/ExifTool/Import.pm +13 -9
  81. data/bin/lib/Image/ExifTool/InDesign.pm +3 -5
  82. data/bin/lib/Image/ExifTool/JPEG.pm +22 -11
  83. data/bin/lib/Image/ExifTool/JPEGDigest.pm +1 -1
  84. data/bin/lib/Image/ExifTool/JSON.pm +3 -3
  85. data/bin/lib/Image/ExifTool/JVC.pm +1 -1
  86. data/bin/lib/Image/ExifTool/Jpeg2000.pm +2 -2
  87. data/bin/lib/Image/ExifTool/Kodak.pm +1233 -58
  88. data/bin/lib/Image/ExifTool/KyoceraRaw.pm +1 -1
  89. data/bin/lib/Image/ExifTool/LNK.pm +1 -1
  90. data/bin/lib/Image/ExifTool/Lang/cs.pm +1 -1
  91. data/bin/lib/Image/ExifTool/Lang/de.pm +33 -24
  92. data/bin/lib/Image/ExifTool/Lang/en_ca.pm +64 -2
  93. data/bin/lib/Image/ExifTool/Lang/en_gb.pm +64 -2
  94. data/bin/lib/Image/ExifTool/Lang/es.pm +8 -4
  95. data/bin/lib/Image/ExifTool/Lang/fi.pm +46 -4
  96. data/bin/lib/Image/ExifTool/Lang/fr.pm +5 -3
  97. data/bin/lib/Image/ExifTool/Lang/it.pm +6 -3
  98. data/bin/lib/Image/ExifTool/Lang/ja.pm +15 -3
  99. data/bin/lib/Image/ExifTool/Lang/ko.pm +5 -2
  100. data/bin/lib/Image/ExifTool/Lang/nl.pm +6 -3
  101. data/bin/lib/Image/ExifTool/Lang/pl.pm +2 -2
  102. data/bin/lib/Image/ExifTool/Lang/ru.pm +1 -1
  103. data/bin/lib/Image/ExifTool/Lang/sv.pm +1 -1
  104. data/bin/lib/Image/ExifTool/Lang/tr.pm +4 -2
  105. data/bin/lib/Image/ExifTool/Lang/zh_cn.pm +1 -1
  106. data/bin/lib/Image/ExifTool/Lang/zh_tw.pm +1 -1
  107. data/bin/lib/Image/ExifTool/Leaf.pm +1 -1
  108. data/bin/lib/Image/ExifTool/Lytro.pm +4 -8
  109. data/bin/lib/Image/ExifTool/M2TS.pm +10 -9
  110. data/bin/lib/Image/ExifTool/MIE.pm +12 -8
  111. data/bin/lib/Image/ExifTool/MIEUnits.pod +1 -1
  112. data/bin/lib/Image/ExifTool/MIFF.pm +1 -1
  113. data/bin/lib/Image/ExifTool/MNG.pm +1 -1
  114. data/bin/lib/Image/ExifTool/MOI.pm +1 -1
  115. data/bin/lib/Image/ExifTool/MPC.pm +1 -1
  116. data/bin/lib/Image/ExifTool/MPEG.pm +2 -3
  117. data/bin/lib/Image/ExifTool/MPF.pm +6 -6
  118. data/bin/lib/Image/ExifTool/MWG.pm +4 -4
  119. data/bin/lib/Image/ExifTool/MXF.pm +2 -2
  120. data/bin/lib/Image/ExifTool/MacOS.pm +184 -34
  121. data/bin/lib/Image/ExifTool/MakerNotes.pm +101 -18
  122. data/bin/lib/Image/ExifTool/Matroska.pm +1 -1
  123. data/bin/lib/Image/ExifTool/Microsoft.pm +5 -3
  124. data/bin/lib/Image/ExifTool/Minolta.pm +89 -62
  125. data/bin/lib/Image/ExifTool/MinoltaRaw.pm +1 -1
  126. data/bin/lib/Image/ExifTool/Motorola.pm +1 -1
  127. data/bin/lib/Image/ExifTool/Nikon.pm +1511 -380
  128. data/bin/lib/Image/ExifTool/NikonCapture.pm +1 -1
  129. data/bin/lib/Image/ExifTool/NikonCustom.pm +2758 -2935
  130. data/bin/lib/Image/ExifTool/Nintendo.pm +1 -1
  131. data/bin/lib/Image/ExifTool/OOXML.pm +1 -1
  132. data/bin/lib/Image/ExifTool/Ogg.pm +1 -1
  133. data/bin/lib/Image/ExifTool/Olympus.pm +47 -8
  134. data/bin/lib/Image/ExifTool/OpenEXR.pm +1 -1
  135. data/bin/lib/Image/ExifTool/Opus.pm +1 -1
  136. data/bin/lib/Image/ExifTool/PCX.pm +138 -0
  137. data/bin/lib/Image/ExifTool/PDF.pm +58 -42
  138. data/bin/lib/Image/ExifTool/PGF.pm +1 -1
  139. data/bin/lib/Image/ExifTool/PICT.pm +1 -1
  140. data/bin/lib/Image/ExifTool/PLIST.pm +12 -5
  141. data/bin/lib/Image/ExifTool/PLUS.pm +1 -1
  142. data/bin/lib/Image/ExifTool/PNG.pm +108 -10
  143. data/bin/lib/Image/ExifTool/PPM.pm +3 -3
  144. data/bin/lib/Image/ExifTool/PSP.pm +1 -1
  145. data/bin/lib/Image/ExifTool/Palm.pm +1 -1
  146. data/bin/lib/Image/ExifTool/Panasonic.pm +299 -31
  147. data/bin/lib/Image/ExifTool/PanasonicRaw.pm +201 -19
  148. data/bin/lib/Image/ExifTool/Pentax.pm +164 -143
  149. data/bin/lib/Image/ExifTool/PhaseOne.pm +12 -5
  150. data/bin/lib/Image/ExifTool/PhotoCD.pm +9 -10
  151. data/bin/lib/Image/ExifTool/PhotoMechanic.pm +1 -1
  152. data/bin/lib/Image/ExifTool/Photoshop.pm +230 -60
  153. data/bin/lib/Image/ExifTool/PostScript.pm +29 -4
  154. data/bin/lib/Image/ExifTool/PrintIM.pm +1 -1
  155. data/bin/lib/Image/ExifTool/Qualcomm.pm +2 -2
  156. data/bin/lib/Image/ExifTool/QuickTime.pm +1539 -279
  157. data/bin/lib/Image/ExifTool/QuickTimeStream.pl +1857 -0
  158. data/bin/lib/Image/ExifTool/README +84 -46
  159. data/bin/lib/Image/ExifTool/RIFF.pm +116 -23
  160. data/bin/lib/Image/ExifTool/RSRC.pm +1 -1
  161. data/bin/lib/Image/ExifTool/RTF.pm +6 -4
  162. data/bin/lib/Image/ExifTool/Radiance.pm +1 -1
  163. data/bin/lib/Image/ExifTool/Rawzor.pm +3 -2
  164. data/bin/lib/Image/ExifTool/Real.pm +1 -1
  165. data/bin/lib/Image/ExifTool/Reconyx.pm +261 -7
  166. data/bin/lib/Image/ExifTool/Red.pm +325 -0
  167. data/bin/lib/Image/ExifTool/Ricoh.pm +3 -7
  168. data/bin/lib/Image/ExifTool/Samsung.pm +95 -25
  169. data/bin/lib/Image/ExifTool/Sanyo.pm +1 -1
  170. data/bin/lib/Image/ExifTool/Scalado.pm +1 -1
  171. data/bin/lib/Image/ExifTool/Shift.pl +26 -12
  172. data/bin/lib/Image/ExifTool/Shortcuts.pm +9 -2
  173. data/bin/lib/Image/ExifTool/Sigma.pm +36 -30
  174. data/bin/lib/Image/ExifTool/SigmaRaw.pm +3 -8
  175. data/bin/lib/Image/ExifTool/Sony.pm +531 -177
  176. data/bin/lib/Image/ExifTool/SonyIDC.pm +63 -3
  177. data/bin/lib/Image/ExifTool/Stim.pm +2 -2
  178. data/bin/lib/Image/ExifTool/TagInfoXML.pm +23 -23
  179. data/bin/lib/Image/ExifTool/TagLookup.pm +6352 -5062
  180. data/bin/lib/Image/ExifTool/TagNames.pod +3024 -565
  181. data/bin/lib/Image/ExifTool/Theora.pm +1 -1
  182. data/bin/lib/Image/ExifTool/Torrent.pm +2 -2
  183. data/bin/lib/Image/ExifTool/Unknown.pm +1 -1
  184. data/bin/lib/Image/ExifTool/VCard.pm +47 -9
  185. data/bin/lib/Image/ExifTool/Validate.pm +391 -99
  186. data/bin/lib/Image/ExifTool/Vorbis.pm +1 -1
  187. data/bin/lib/Image/ExifTool/WTV.pm +319 -0
  188. data/bin/lib/Image/ExifTool/WriteCanonRaw.pl +1 -1
  189. data/bin/lib/Image/ExifTool/WriteExif.pl +91 -18
  190. data/bin/lib/Image/ExifTool/WriteIPTC.pl +6 -6
  191. data/bin/lib/Image/ExifTool/WritePDF.pl +13 -12
  192. data/bin/lib/Image/ExifTool/WritePNG.pl +1 -1
  193. data/bin/lib/Image/ExifTool/WritePhotoshop.pl +1 -1
  194. data/bin/lib/Image/ExifTool/WritePostScript.pl +2 -2
  195. data/bin/lib/Image/ExifTool/WriteQuickTime.pl +764 -121
  196. data/bin/lib/Image/ExifTool/WriteXMP.pl +176 -67
  197. data/bin/lib/Image/ExifTool/Writer.pl +490 -246
  198. data/bin/lib/Image/ExifTool/XMP.pm +216 -76
  199. data/bin/lib/Image/ExifTool/XMP2.pl +54 -10
  200. data/bin/lib/Image/ExifTool/XMPStruct.pl +14 -11
  201. data/bin/lib/Image/ExifTool/ZIP.pm +60 -15
  202. data/bin/lib/Image/ExifTool/iWork.pm +12 -5
  203. data/bin/perl-Image-ExifTool.spec +46 -44
  204. data/lib/exiftool_vendored/version.rb +1 -1
  205. metadata +14 -4
@@ -0,0 +1,1857 @@
1
+ #------------------------------------------------------------------------------
2
+ # File: QuickTimeStream.pl
3
+ #
4
+ # Description: Extract embedded information from QuickTime movie 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
+ #------------------------------------------------------------------------------
14
+ package Image::ExifTool::QuickTime;
15
+
16
+ use strict;
17
+
18
+ sub Process_tx3g($$$);
19
+ sub ProcessFreeGPS($$$);
20
+ sub ProcessFreeGPS2($$$);
21
+
22
+ # QuickTime data types that have ExifTool equivalents
23
+ # (ref https://developer.apple.com/library/content/documentation/QuickTime/QTFF/Metadata/Metadata.html#//apple_ref/doc/uid/TP40000939-CH1-SW35)
24
+ my %qtFmt = (
25
+ 0 => 'undef',
26
+ 1 => 'string', # (UTF-8)
27
+ # 2 - UTF-16
28
+ # 3 - shift-JIS
29
+ # 4 - UTF-8 sort
30
+ # 5 - UTF-16 sort
31
+ # 13 - JPEG image
32
+ # 14 - PNG image
33
+ # 21 - signed integer (1,2,3 or 4 bytes)
34
+ # 22 - unsigned integer (1,2,3 or 4 bytes)
35
+ 23 => 'float',
36
+ 24 => 'double',
37
+ # 27 - BMP image
38
+ # 28 - QuickTime atom
39
+ 65 => 'int8s',
40
+ 66 => 'int16s',
41
+ 67 => 'int32s',
42
+ 70 => 'float', # float[2] x,y
43
+ 71 => 'float', # float[2] width,height
44
+ 72 => 'float', # float[4] x,y,width,height
45
+ 74 => 'int64s',
46
+ 75 => 'int8u',
47
+ 76 => 'int16u',
48
+ 77 => 'int32u',
49
+ 78 => 'int64u',
50
+ 79 => 'float', # float[9] transform matrix
51
+ 80 => 'float', # float[8] face coordinates
52
+ );
53
+
54
+ # maximums for validating H,M,S,d,m,Y from "freeGPS " metadata
55
+ my @dateMax = ( 24, 59, 59, 2200, 12, 31 );
56
+
57
+ # typical (minimum?) size of freeGPS block
58
+ my $gpsBlockSize = 0x8000;
59
+
60
+ # conversion factors
61
+ my $knotsToKph = 1.852; # knots --> km/h
62
+ my $mpsToKph = 3.6; # m/s --> km/h
63
+
64
+ # handler types to process based on MetaFormat/OtherFormat
65
+ my %processByMetaFormat = (
66
+ meta => 1, # ('CTMD' in CR3 images, 'priv' unknown in DJI video)
67
+ data => 1, # ('RVMI')
68
+ sbtl => 1, # (subtitle; 'tx3g' in Yuneec drone videos)
69
+ );
70
+
71
+ # data lengths for each INSV record type
72
+ my %insvDataLen = (
73
+ 0x300 => 56,
74
+ 0x400 => 16,
75
+ 0x700 => 53,
76
+ );
77
+
78
+ # tags extracted from various QuickTime data streams
79
+ %Image::ExifTool::QuickTime::Stream = (
80
+ GROUPS => { 2 => 'Location' },
81
+ NOTES => q{
82
+ Timed metadata extracted from QuickTime movie data and some AVI videos when
83
+ the ExtractEmbedded option is used.
84
+ },
85
+ VARS => { NO_ID => 1 },
86
+ GPSLatitude => { PrintConv => 'Image::ExifTool::GPS::ToDMS($self, $val, 1, "N")' },
87
+ GPSLongitude => { PrintConv => 'Image::ExifTool::GPS::ToDMS($self, $val, 1, "E")' },
88
+ GPSAltitude => { PrintConv => '(sprintf("%.4f", $val) + 0) . " m"' }, # round to 4 decimals
89
+ GPSSpeed => { PrintConv => 'sprintf("%.4f", $val) + 0' }, # round to 4 decimals
90
+ GPSSpeedRef => { PrintConv => { K => 'km/h', M => 'mph', N => 'knots' } },
91
+ GPSTrack => { PrintConv => 'sprintf("%.4f", $val) + 0' }, # round to 4 decimals
92
+ GPSTrackRef => { PrintConv => { M => 'Magnetic North', T => 'True North' } },
93
+ GPSDateTime => { PrintConv => '$self->ConvertDateTime($val)', Groups => { 2 => 'Time' } },
94
+ GPSTimeStamp => { PrintConv => 'Image::ExifTool::GPS::PrintTimeStamp($val)', Groups => { 2 => 'Time' } },
95
+ GPSSatellites=> { },
96
+ GPSDOP => { Description => 'GPS Dilution Of Precision' },
97
+ CameraDateTime=>{ PrintConv => '$self->ConvertDateTime($val)', Groups => { 2 => 'Time' } },
98
+ Accelerometer=> { Notes => 'right/up/backward acceleration in units of g' },
99
+ AngularVelocity => { },
100
+ RawGSensor => {
101
+ # (same as GSensor, but offset by some unknown value)
102
+ ValueConv => 'my @a=split " ",$val; $_/=1000 foreach @a; "@a"',
103
+ },
104
+ Text => { Groups => { 2 => 'Other' } },
105
+ TimeCode => { Groups => { 2 => 'Video' } },
106
+ FrameNumber => { Groups => { 2 => 'Video' } },
107
+ SampleTime => { Groups => { 2 => 'Video' }, PrintConv => 'ConvertDuration($val)', Notes => 'sample decoding time' },
108
+ SampleDuration=>{ Groups => { 2 => 'Video' }, PrintConv => 'ConvertDuration($val)' },
109
+ UserLabel => { Groups => { 2 => 'Other' } },
110
+ #
111
+ # timed metadata decoded based on MetaFormat (format of 'meta' or 'data' sample description)
112
+ # [or HandlerType, or specific 'vide' type if specified]
113
+ #
114
+ mebx => {
115
+ Name => 'mebx',
116
+ SubDirectory => {
117
+ TagTable => 'Image::ExifTool::QuickTime::Keys',
118
+ ProcessProc => \&Process_mebx,
119
+ },
120
+ },
121
+ gpmd => {
122
+ Name => 'gpmd',
123
+ SubDirectory => { TagTable => 'Image::ExifTool::GoPro::GPMF' },
124
+ },
125
+ fdsc => {
126
+ Name => 'fdsc',
127
+ Condition => '$$valPt =~ /^GPRO/',
128
+ # (other types of "fdsc" samples aren't yet parsed: /^GP\x00/ and /^GP\x04/)
129
+ SubDirectory => { TagTable => 'Image::ExifTool::GoPro::fdsc' },
130
+ },
131
+ rtmd => {
132
+ Name => 'rtmd',
133
+ SubDirectory => { TagTable => 'Image::ExifTool::Sony::rtmd' },
134
+ },
135
+ CTMD => { # (Canon Timed MetaData)
136
+ Name => 'CTMD',
137
+ SubDirectory => { TagTable => 'Image::ExifTool::Canon::CTMD' },
138
+ },
139
+ tx3g => {
140
+ Name => 'tx3g',
141
+ SubDirectory => { TagTable => 'Image::ExifTool::QuickTime::tx3g' },
142
+ },
143
+ RVMI => [{ # data "OtherFormat" written by unknown software
144
+ Name => 'RVMI_gReV',
145
+ Condition => '$$valPt =~ /^gReV/', # GPS data
146
+ SubDirectory => {
147
+ TagTable => 'Image::ExifTool::QuickTime::RVMI_gReV',
148
+ ByteOrder => 'Little-endian',
149
+ },
150
+ },{
151
+ Name => 'RVMI_sReV',
152
+ Condition => '$$valPt =~ /^sReV/', # sensor data
153
+ SubDirectory => {
154
+ TagTable => 'Image::ExifTool::QuickTime::RVMI_sReV',
155
+ ByteOrder => 'Little-endian',
156
+ },
157
+ # (there is also "tReV" data that hasn't been decoded yet)
158
+ }],
159
+ camm => [{
160
+ Name => 'camm0',
161
+ # (according to the spec. the first 2 bytes are reserved and should be zero,
162
+ # but I have a sample where these bytes are non-zero, so allow anything here)
163
+ Condition => '$$valPt =~ /^..\0\0/s',
164
+ SubDirectory => {
165
+ TagTable => 'Image::ExifTool::QuickTime::camm0',
166
+ ByteOrder => 'Little-Endian',
167
+ },
168
+ },{
169
+ Name => 'camm1',
170
+ Condition => '$$valPt =~ /^..\x01\0/s',
171
+ SubDirectory => {
172
+ TagTable => 'Image::ExifTool::QuickTime::camm1',
173
+ ByteOrder => 'Little-Endian',
174
+ },
175
+ },{ # (written by Insta360) - [HandlerType, not MetaFormat]
176
+ Name => 'camm2',
177
+ Condition => '$$valPt =~ /^..\x02\0/s',
178
+ SubDirectory => {
179
+ TagTable => 'Image::ExifTool::QuickTime::camm2',
180
+ ByteOrder => 'Little-Endian',
181
+ },
182
+ },{
183
+ Name => 'camm3',
184
+ Condition => '$$valPt =~ /^..\x03\0/s',
185
+ SubDirectory => {
186
+ TagTable => 'Image::ExifTool::QuickTime::camm3',
187
+ ByteOrder => 'Little-Endian',
188
+ },
189
+ },{
190
+ Name => 'camm4',
191
+ Condition => '$$valPt =~ /^..\x04\0/s',
192
+ SubDirectory => {
193
+ TagTable => 'Image::ExifTool::QuickTime::camm4',
194
+ ByteOrder => 'Little-Endian',
195
+ },
196
+ },{
197
+ Name => 'camm5',
198
+ Condition => '$$valPt =~ /^..\x05\0/s',
199
+ SubDirectory => {
200
+ TagTable => 'Image::ExifTool::QuickTime::camm5',
201
+ ByteOrder => 'Little-Endian',
202
+ },
203
+ },{
204
+ Name => 'camm6',
205
+ Condition => '$$valPt =~ /^..\x06\0/s',
206
+ SubDirectory => {
207
+ TagTable => 'Image::ExifTool::QuickTime::camm6',
208
+ ByteOrder => 'Little-Endian',
209
+ },
210
+ },{
211
+ Name => 'camm7',
212
+ Condition => '$$valPt =~ /^..\x07\0/s',
213
+ SubDirectory => {
214
+ TagTable => 'Image::ExifTool::QuickTime::camm7',
215
+ ByteOrder => 'Little-Endian',
216
+ },
217
+ }],
218
+ JPEG => { # (in CR3 images) - [vide HandlerType with JPEG in SampleDescription, not MetaFormat]
219
+ Name => 'JpgFromRaw',
220
+ Groups => { 2 => 'Preview' },
221
+ RawConv => '$self->ValidateImage(\$val,$tag)',
222
+ },
223
+ text => { # (TomTom Bandit MP4) - [sbtl HandlerType with 'text' in SampleDescription]
224
+ Name => 'PreviewInfo',
225
+ Condition => 'length $$valPt > 12 and Get32u($valPt,4) == length($$valPt) and $$valPt =~ /^.{8}\xff\xd8\xff/s',
226
+ SubDirectory => { TagTable => 'Image::ExifTool::QuickTime::PreviewInfo' },
227
+ },
228
+ INSV => { SubDirectory => { TagTable => 'Image::ExifTool::QuickTime::INSV_MakerNotes' } },
229
+ Unknown00 => { Unknown => 1 },
230
+ Unknown01 => { Unknown => 1 },
231
+ Unknown02 => { Unknown => 1 },
232
+ Unknown03 => { Unknown => 1 },
233
+ );
234
+
235
+ # tags found in 'camm' type 0 timed metadata (ref 4)
236
+ %Image::ExifTool::QuickTime::camm0 = (
237
+ PROCESS_PROC => \&Image::ExifTool::ProcessBinaryData,
238
+ GROUPS => { 2 => 'Location' },
239
+ FIRST_ENTRY => 0,
240
+ NOTES => q{
241
+ The camm0 through camm7 tables define tags extracted from the Google Street
242
+ View Camera Motion Metadata of MP4 videos. See
243
+ L<https://developers.google.com/streetview/publish/camm-spec> for the
244
+ specification.
245
+ },
246
+ 4 => {
247
+ Name => 'AngleAxis',
248
+ Notes => 'angle axis orientation in radians in local coordinate system',
249
+ Format => 'float[3]',
250
+ },
251
+ );
252
+
253
+ # tags found in 'camm' type 1 timed metadata (ref 4)
254
+ %Image::ExifTool::QuickTime::camm1 = (
255
+ PROCESS_PROC => \&Image::ExifTool::ProcessBinaryData,
256
+ GROUPS => { 2 => 'Camera' },
257
+ FIRST_ENTRY => 0,
258
+ 4 => {
259
+ Name => 'PixelExposureTime',
260
+ Format => 'int32s',
261
+ ValueConv => '$val * 1e-9',
262
+ PrintConv => 'sprintf("%.4g ms", $val * 1000)',
263
+ },
264
+ 8 => {
265
+ Name => 'RollingShutterSkewTime',
266
+ Format => 'int32s',
267
+ ValueConv => '$val * 1e-9',
268
+ PrintConv => 'sprintf("%.4g ms", $val * 1000)',
269
+ },
270
+ );
271
+
272
+ # tags found in 'camm' type 2 timed metadata (ref PH, Insta360Pro)
273
+ %Image::ExifTool::QuickTime::camm2 = (
274
+ PROCESS_PROC => \&Image::ExifTool::ProcessBinaryData,
275
+ GROUPS => { 2 => 'Location' },
276
+ FIRST_ENTRY => 0,
277
+ 4 => {
278
+ Name => 'AngularVelocity',
279
+ Notes => 'gyro angular velocity about X, Y and Z axes in rad/s',
280
+ Format => 'float[3]',
281
+ },
282
+ );
283
+
284
+ # tags found in 'camm' type 3 timed metadata (ref PH, Insta360Pro)
285
+ %Image::ExifTool::QuickTime::camm3 = (
286
+ PROCESS_PROC => \&Image::ExifTool::ProcessBinaryData,
287
+ GROUPS => { 2 => 'Location' },
288
+ FIRST_ENTRY => 0,
289
+ 4 => {
290
+ Name => 'Acceleration',
291
+ Notes => 'acceleration in the X, Y and Z directions in m/s^2',
292
+ Format => 'float[3]',
293
+ },
294
+ );
295
+
296
+ # tags found in 'camm' type 4 timed metadata (ref 4)
297
+ %Image::ExifTool::QuickTime::camm4 = (
298
+ PROCESS_PROC => \&Image::ExifTool::ProcessBinaryData,
299
+ GROUPS => { 2 => 'Location' },
300
+ FIRST_ENTRY => 0,
301
+ 4 => {
302
+ Name => 'Position',
303
+ Notes => 'X, Y, Z position in local coordinate system',
304
+ Format => 'float[3]',
305
+ },
306
+ );
307
+
308
+ # tags found in 'camm' type 5 timed metadata (ref 4)
309
+ %Image::ExifTool::QuickTime::camm5 = (
310
+ PROCESS_PROC => \&Image::ExifTool::ProcessBinaryData,
311
+ GROUPS => { 2 => 'Location' },
312
+ FIRST_ENTRY => 0,
313
+ 4 => {
314
+ Name => 'GPSLatitude',
315
+ Format => 'double',
316
+ ValueConv => 'Image::ExifTool::GPS::ToDegrees($val, 1)',
317
+ PrintConv => 'Image::ExifTool::GPS::ToDMS($self, $val, 1, "N")',
318
+ },
319
+ 12 => {
320
+ Name => 'GPSLongitude',
321
+ Format => 'double',
322
+ ValueConv => 'Image::ExifTool::GPS::ToDegrees($val, 1)',
323
+ PrintConv => 'Image::ExifTool::GPS::ToDMS($self, $val, 1, "E")',
324
+ },
325
+ 20 => {
326
+ Name => 'GPSAltitude',
327
+ Format => 'double',
328
+ PrintConv => '$_ = sprintf("%.6f", $val); s/\.?0+$//; "$_ m"',
329
+ },
330
+ );
331
+
332
+ # tags found in 'camm' type 6 timed metadata (ref PH/4, Insta360)
333
+ %Image::ExifTool::QuickTime::camm6 = (
334
+ PROCESS_PROC => \&Image::ExifTool::ProcessBinaryData,
335
+ GROUPS => { 2 => 'Location' },
336
+ FIRST_ENTRY => 0,
337
+ 0x04 => {
338
+ Name => 'GPSDateTime',
339
+ Groups => { 2 => 'Time' },
340
+ Format => 'double',
341
+ ValueConv => q{
342
+ my $str = ConvertUnixTime($val);
343
+ my $frac = $val - int($val);
344
+ if ($frac != 0) {
345
+ $frac = sprintf('%.6f', $frac);
346
+ $frac =~ s/^0//;
347
+ $frac =~ s/0+$//;
348
+ $str .= $frac;
349
+ }
350
+ return $str . 'Z';
351
+ },
352
+ PrintConv => '$self->ConvertDateTime($val)',
353
+ },
354
+ 0x0c => {
355
+ Name => 'GPSMeasureMode',
356
+ Format => 'int32u',
357
+ PrintConv => {
358
+ 0 => 'No Measurement',
359
+ 2 => '2-Dimensional Measurement',
360
+ 3 => '3-Dimensional Measurement',
361
+ },
362
+ },
363
+ 0x10 => {
364
+ Name => 'GPSLatitude',
365
+ Format => 'double',
366
+ ValueConv => 'Image::ExifTool::GPS::ToDegrees($val, 1)',
367
+ PrintConv => 'Image::ExifTool::GPS::ToDMS($self, $val, 1, "N")',
368
+ },
369
+ 0x18 => {
370
+ Name => 'GPSLongitude',
371
+ Format => 'double',
372
+ ValueConv => 'Image::ExifTool::GPS::ToDegrees($val, 1)',
373
+ PrintConv => 'Image::ExifTool::GPS::ToDMS($self, $val, 1, "E")',
374
+ },
375
+ 0x20 => {
376
+ Name => 'GPSAltitude',
377
+ Format => 'float',
378
+ PrintConv => '$_ = sprintf("%.3f", $val); s/\.?0+$//; "$_ m"',
379
+ },
380
+ 0x24 => { Name => 'GPSHorizontalAccuracy', Format => 'float', Notes => 'metres' },
381
+ 0x28 => { Name => 'GPSVerticalAccuracy', Format => 'float' },
382
+ 0x2c => { Name => 'GPSVelocityEast', Format => 'float', Notes => 'm/s' },
383
+ 0x30 => { Name => 'GPSVelocityNorth', Format => 'float' },
384
+ 0x34 => { Name => 'GPSVelocityUp', Format => 'float' },
385
+ 0x38 => { Name => 'GPSSpeedAccuracy', Format => 'float' },
386
+ );
387
+
388
+ # tags found in 'camm' type 7 timed metadata (ref 4)
389
+ %Image::ExifTool::QuickTime::camm7 = (
390
+ PROCESS_PROC => \&Image::ExifTool::ProcessBinaryData,
391
+ GROUPS => { 2 => 'Location' },
392
+ FIRST_ENTRY => 0,
393
+ 4 => {
394
+ Name => 'MagneticField',
395
+ Format => 'float[3]',
396
+ Notes => 'microtesla',
397
+ },
398
+ );
399
+
400
+ # preview image stored by TomTom Bandit ActionCam
401
+ %Image::ExifTool::QuickTime::PreviewInfo = (
402
+ PROCESS_PROC => \&Image::ExifTool::ProcessBinaryData,
403
+ FIRST_ENTRY => 0,
404
+ NOTES => 'Preview stored by TomTom Bandit ActionCam.',
405
+ 8 => {
406
+ Name => 'PreviewImage',
407
+ Groups => { 2 => 'Preview' },
408
+ Binary => 1,
409
+ Format => 'undef[$size-8]',
410
+ },
411
+ );
412
+
413
+ # tags found in 'RVMI' 'gReV' timed metadata (ref PH)
414
+ %Image::ExifTool::QuickTime::RVMI_gReV = (
415
+ PROCESS_PROC => \&Image::ExifTool::ProcessBinaryData,
416
+ GROUPS => { 2 => 'Location' },
417
+ FIRST_ENTRY => 0,
418
+ NOTES => 'GPS information extracted from the RVMI box of MOV videos.',
419
+ 4 => {
420
+ Name => 'GPSLatitude',
421
+ Format => 'int32s',
422
+ ValueConv => 'Image::ExifTool::GPS::ToDegrees($val/1e6, 1)',
423
+ PrintConv => 'Image::ExifTool::GPS::ToDMS($self, $val, 1, "N")',
424
+ },
425
+ 8 => {
426
+ Name => 'GPSLongitude',
427
+ Format => 'int32s',
428
+ ValueConv => 'Image::ExifTool::GPS::ToDegrees($val/1e6, 1)',
429
+ PrintConv => 'Image::ExifTool::GPS::ToDMS($self, $val, 1, "E")',
430
+ },
431
+ # 12 - int32s: space for altitude? (always zero in my sample)
432
+ 16 => {
433
+ Name => 'GPSSpeed', # km/h
434
+ Format => 'int16s',
435
+ ValueConv => '$val / 10',
436
+ },
437
+ 18 => {
438
+ Name => 'GPSTrack',
439
+ Format => 'int16u',
440
+ ValueConv => '$val * 2',
441
+ },
442
+ );
443
+
444
+ # tags found in 'RVMI' 'sReV' timed metadata (ref PH)
445
+ %Image::ExifTool::QuickTime::RVMI_sReV = (
446
+ PROCESS_PROC => \&Image::ExifTool::ProcessBinaryData,
447
+ GROUPS => { 2 => 'Location' },
448
+ FIRST_ENTRY => 0,
449
+ NOTES => q{
450
+ G-sensor information extracted from the RVMI box of MOV videos.
451
+ },
452
+ 4 => {
453
+ Name => 'GSensor',
454
+ Format => 'int16s[3]', # X Y Z
455
+ ValueConv => 'my @a=split " ",$val; $_/=1000 foreach @a; "@a"',
456
+ },
457
+ );
458
+
459
+ # tags found in 'tx3g' sbtl timed metadata (ref PH)
460
+ %Image::ExifTool::QuickTime::tx3g = (
461
+ PROCESS_PROC => \&Process_tx3g,
462
+ GROUPS => { 2 => 'Location' },
463
+ FIRST_ENTRY => 0,
464
+ NOTES => 'Tags extracted from the tx3g sbtl timed metadata of Yuneec drones.',
465
+ Lat => {
466
+ Name => 'GPSLatitude',
467
+ PrintConv => 'Image::ExifTool::GPS::ToDMS($self, $val, 1, "N")',
468
+ },
469
+ Lon => {
470
+ Name => 'GPSLongitude',
471
+ PrintConv => 'Image::ExifTool::GPS::ToDMS($self, $val, 1, "E")',
472
+ },
473
+ Alt => {
474
+ Name => 'GPSAltitude',
475
+ ValueConv => '$val =~ s/\s*m$//; $val', # remove " m"
476
+ PrintConv => '"$val m"', # add it back again
477
+ },
478
+ Yaw => 'Yaw',
479
+ Pitch => 'Pitch',
480
+ Roll => 'Roll',
481
+ GimYaw => 'GimbalYaw',
482
+ GimPitch => 'GimbalPitch',
483
+ GimRoll => 'GimbalRoll',
484
+ );
485
+
486
+ %Image::ExifTool::QuickTime::INSV_MakerNotes = (
487
+ GROUPS => { 1 => 'MakerNotes', 2 => 'Camera' },
488
+ 0x0a => 'SerialNumber',
489
+ 0x12 => 'Model',
490
+ 0x1a => 'Firmware',
491
+ 0x2a => {
492
+ Name => 'Parameters',
493
+ ValueConv => '$val =~ tr/_/ /; $val',
494
+ },
495
+ );
496
+
497
+ #------------------------------------------------------------------------------
498
+ # Save information from keys in OtherSampleDesc directory for processing timed metadata
499
+ # Inputs: 0) ExifTool object ref, 1) dirInfo ref, 2) tag table ref
500
+ # Returns: 1 on success
501
+ # (ref "Timed Metadata Media" here:
502
+ # https://developer.apple.com/library/content/documentation/QuickTime/QTFF/QTFFChap3/qtff3.html)
503
+ sub SaveMetaKeys($$$)
504
+ {
505
+ local $_;
506
+ my ($et, $dirInfo, $tagTbl) = @_;
507
+ my $dataPt = $$dirInfo{DataPt};
508
+ my $dirLen = length $$dataPt;
509
+ return 0 unless $dirLen > 8;
510
+ my $pos = 0;
511
+ my $verbose = $$et{OPTIONS}{Verbose};
512
+ my $oldIndent = $$et{INDENT};
513
+ my $ee = $$et{ee};
514
+ $ee or $ee = $$et{ee} = { };
515
+
516
+ $verbose and $et->VerboseDir($$dirInfo{DirName}, undef, $dirLen);
517
+
518
+ # loop through metadata key table
519
+ while ($pos + 8 < $dirLen) {
520
+ my $size = Get32u($dataPt, $pos);
521
+ my $id = substr($$dataPt, $pos+4, 4);
522
+ my $end = $pos + $size;
523
+ $end = $dirLen if $end > $dirLen;
524
+ $pos += 8;
525
+ my ($tagID, $format, $pid);
526
+ if ($verbose) {
527
+ $pid = PrintableTagID($id,1);
528
+ $et->VPrint(0, "$oldIndent+ [Metdata Key entry, Local ID=$pid, $size bytes]\n");
529
+ $$et{INDENT} .= '| ';
530
+ }
531
+
532
+ while ($pos + 4 < $end) {
533
+ my $len = unpack("x${pos}N", $$dataPt);
534
+ last if $len < 8 or $pos + $len > $end;
535
+ my $tag = substr($$dataPt, $pos + 4, 4);
536
+ $pos += 8; $len -= 8;
537
+ my $val = substr($$dataPt, $pos, $len);
538
+ $pos += $len;
539
+ my $str;
540
+ if ($tag eq 'keyd') {
541
+ ($tagID = $val) =~ s/^(mdta|fiel)com\.apple\.quicktime\.//;
542
+ $tagID = "Tag_$val" unless $tagID;
543
+ ($str = $val) =~ s/(.{4})/$1 / if $verbose;
544
+ } elsif ($tag eq 'dtyp') {
545
+ next if length $val < 4;
546
+ if (length $val >= 4) {
547
+ my $ns = unpack('N', $val);
548
+ if ($ns == 0) {
549
+ length $val >= 8 or $et->Warn('Short dtyp data'), next;
550
+ $str = unpack('x4N',$val);
551
+ $format = $qtFmt{$str} || 'undef';
552
+ } elsif ($ns == 1) {
553
+ $str = substr($val, 4);
554
+ $format = 'undef';
555
+ } else {
556
+ $format = 'undef';
557
+ }
558
+ $str .= " ($format)" if $verbose and defined $str;
559
+ }
560
+ }
561
+ if ($verbose > 1) {
562
+ if (defined $str) {
563
+ $str =~ tr/\x00-\x1f\x7f-\xff/./;
564
+ $str = " = $str";
565
+ } else {
566
+ $str = '';
567
+ }
568
+ $et->VPrint(1, $$et{INDENT}."- Tag '".PrintableTagID($tag)."' ($len bytes)$str\n");
569
+ $et->VerboseDump(\$val);
570
+ }
571
+ }
572
+ if (defined $tagID and defined $format) {
573
+ if ($verbose) {
574
+ my $t2 = PrintableTagID($tagID);
575
+ $et->VPrint(0, "$$et{INDENT}Added Local ID $pid = $t2 ($format)\n");
576
+ }
577
+ $$ee{'keys'}{$id} = { TagID => $tagID, Format => $format };
578
+ }
579
+ $$et{INDENT} = $oldIndent;
580
+ }
581
+ return 1;
582
+ }
583
+
584
+ #------------------------------------------------------------------------------
585
+ # We found some tags for this sample, so set document number and save timing information
586
+ # Inputs: 0) ExifTool ref, 1) tag table ref, 2) sample time, 3) sample duration
587
+ sub FoundSomething($$;$$)
588
+ {
589
+ my ($et, $tagTbl, $time, $dur) = @_;
590
+ $$et{DOC_NUM} = ++$$et{DOC_COUNT};
591
+ $et->HandleTag($tagTbl, SampleTime => $time) if defined $time;
592
+ $et->HandleTag($tagTbl, SampleDuration => $dur) if defined $dur;
593
+ }
594
+
595
+ #------------------------------------------------------------------------------
596
+ # Parse textual metadata
597
+ # Inputs: 0) ExifTool ref, 1) tag table ref, 2) data ref
598
+ sub ParseText($$$)
599
+ {
600
+ my ($et, $tagTbl, $buffPt) = @_;
601
+ while ($$buffPt =~ /\$(\w+)([^\$]*)/g) {
602
+ my ($tag, $dat) = ($1, $2);
603
+ 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+)/) {
604
+ $et->HandleTag($tagTbl, GPSLatitude => (($5 || 0) + $6/60) * ($7 eq 'N' ? 1 : -1));
605
+ $et->HandleTag($tagTbl, GPSLongitude => (($8 || 0) + $9/60) * ($10 eq 'E' ? 1 : -1));
606
+ if (length $11) {
607
+ $et->HandleTag($tagTbl, GPSSpeed => $11 * $knotsToKph);
608
+ $et->HandleTag($tagTbl, GPSSpeedRef => 'K');
609
+ }
610
+ if (length $12) {
611
+ $et->HandleTag($tagTbl, GPSTrack => $11);
612
+ $et->HandleTag($tagTbl, GPSTrackRef => 'T');
613
+ }
614
+ my $year = $15 + ($15 >= 70 ? 1900 : 2000);
615
+ my $str = sprintf('%.4d:%.2d:%.2d %.2d:%.2d:%.2dZ', $year, $14, $13, $1, $2, $3);
616
+ $et->HandleTag($tagTbl, GPSDateTime => $str);
617
+ } elsif ($tag eq 'BEGINGSENSOR' and $dat =~ /^:([-+]\d+\.\d+):([-+]\d+\.\d+):([-+]\d+\.\d+)/) {
618
+ $et->HandleTag($tagTbl, Accelerometer => "$1 $2 $3");
619
+ } elsif ($tag eq 'TIME' and $dat =~ /^:(\d+)/) {
620
+ $et->HandleTag($tagTbl, TimeCode => $1 / ($$et{MediaTS} || 1));
621
+ } elsif ($tag eq 'BEGIN') {
622
+ $et->HandleTag($tagTbl, Text => $dat) if length $dat;
623
+ } elsif ($tag ne 'END') {
624
+ $et->HandleTag($tagTbl, Text => "\$$tag$dat");
625
+ }
626
+ }
627
+ }
628
+
629
+ #------------------------------------------------------------------------------
630
+ # Exract embedded metadata from media samples
631
+ # Inputs: 0) ExifTool ref
632
+ # Notes: Also accesses ExifTool RAF*, SET_GROUP1, HandlerType, MetaFormat,
633
+ # ee*, and avcC elements (* = must exist)
634
+ sub ProcessSamples($)
635
+ {
636
+ my $et = shift;
637
+ my ($raf, $ee) = @$et{qw(RAF ee)};
638
+ my ($i, $buff, $pos, $hdrLen, $hdrFmt, @time, @dur, $oldIndent);
639
+
640
+ return unless $ee;
641
+ delete $$et{ee}; # use only once
642
+
643
+ # only process specific types of video streams
644
+ my $type = $$et{HandlerType} || '';
645
+ if ($type eq 'vide') {
646
+ if ($$ee{avcC}) { $type = 'avcC' }
647
+ elsif ($$ee{JPEG}) { $type = 'JPEG' }
648
+ else { return }
649
+ }
650
+
651
+ my ($start, $size) = @$ee{qw(start size)};
652
+ #
653
+ # determine sample start offsets from chunk offsets (stco) and sample-to-chunk table (stsc),
654
+ # and sample time/duration from time-to-sample (stts)
655
+ #
656
+ unless ($start and $size) {
657
+ return unless $size;
658
+ my ($stco, $stsc, $stts) = @$ee{qw(stco stsc stts)};
659
+ return unless $stco and $stsc and @$stsc;
660
+ $start = [ ];
661
+ my ($nextChunk, $iChunk) = (0, 1);
662
+ my ($chunkStart, $startChunk, $samplesPerChunk, $descIdx, $timeCount, $timeDelta, $time);
663
+ if ($stts and @$stts > 1) {
664
+ $time = 0;
665
+ $timeCount = shift @$stts;
666
+ $timeDelta = shift @$stts;
667
+ }
668
+ my $ts = $$et{MediaTS} || 1;
669
+ foreach $chunkStart (@$stco) {
670
+ if ($iChunk >= $nextChunk and @$stsc) {
671
+ ($startChunk, $samplesPerChunk, $descIdx) = @{shift @$stsc};
672
+ $nextChunk = $$stsc[0][0] if @$stsc;
673
+ }
674
+ @$size < @$start + $samplesPerChunk and $et->WarnOnce('Sample size error'), return;
675
+ my $sampleStart = $chunkStart;
676
+ for ($i=0; ; ) {
677
+ push @$start, $sampleStart;
678
+ if (defined $time) {
679
+ until ($timeCount) {
680
+ if (@$stts < 2) {
681
+ undef $time;
682
+ last;
683
+ }
684
+ $timeCount = shift @$stts;
685
+ $timeDelta = shift @$stts;
686
+ }
687
+ push @time, $time / $ts;
688
+ push @dur, $timeDelta / $ts;
689
+ $time += $timeDelta;
690
+ --$timeCount;
691
+ }
692
+ # (eventually should use the description indices: $descIdx)
693
+ last if ++$i >= $samplesPerChunk;
694
+ $sampleStart += $$size[$#$start];
695
+ }
696
+ ++$iChunk;
697
+ }
698
+ @$start == @$size or $et->WarnOnce('Incorrect sample start/size count'), return;
699
+ }
700
+ #
701
+ # extract and parse the sample data
702
+ #
703
+ my $tagTbl = GetTagTable('Image::ExifTool::QuickTime::Stream');
704
+ my $verbose = $et->Options('Verbose');
705
+ my $metaFormat = $$et{MetaFormat} || '';
706
+ my $tell = $raf->Tell();
707
+
708
+ if ($verbose) {
709
+ $et->VPrint(0, "---- Extract Embedded ----\n");
710
+ $oldIndent = $$et{INDENT};
711
+ $$et{INDENT} = '';
712
+ }
713
+ # get required information from avcC box if parsing video data
714
+ if ($type eq 'avcC') {
715
+ $hdrLen = (Get8u(\$$ee{avcC}, 4) & 0x03) + 1;
716
+ $hdrFmt = ($hdrLen == 4 ? 'N' : $hdrLen == 2 ? 'n' : 'C');
717
+ require Image::ExifTool::H264;
718
+ }
719
+ # loop through all samples
720
+ for ($i=0; $i<@$start and $i<@$size; ++$i) {
721
+
722
+ # read the sample data
723
+ my $size = $$size[$i];
724
+ next unless $raf->Seek($$start[$i], 0) and $raf->Read($buff, $size) == $size;
725
+
726
+ if ($type eq 'avcC') {
727
+ next if length($buff) <= $hdrLen;
728
+ # scan through all NAL units and send them to ParseH264Video()
729
+ for ($pos=0; ; ) {
730
+ my $len = unpack("x$pos$hdrFmt", $buff);
731
+ last if $pos + $hdrLen + $len > length($buff);
732
+ my $tmp = "\0\0\0\x01" . substr($buff, $pos+$hdrLen, $len);
733
+ Image::ExifTool::H264::ParseH264Video($et, \$tmp);
734
+ $pos += $hdrLen + $len;
735
+ last if $pos + $hdrLen >= length($buff);
736
+ }
737
+ next;
738
+ }
739
+ if ($verbose > 1) {
740
+ my $hdr = $$et{SET_GROUP1} ? "$$et{SET_GROUP1} Type='${type}' Format='${metaFormat}'" : "Type='${type}'";
741
+ $et->VPrint(1, "${hdr}, Sample ".($i+1).' of '.scalar(@$start)." ($size bytes)\n");
742
+ $et->VerboseDump(\$buff, Addr => $$start[$i]);
743
+ }
744
+ if ($type eq 'text') {
745
+
746
+ FoundSomething($et, $tagTbl, $time[$i], $dur[$i]);
747
+ unless ($buff =~ /^\$BEGIN/) {
748
+ # remove ending "encd" box if it exists
749
+ $buff =~ s/\0\0\0\x0cencd\0\0\x01\0$// and $size -= 12;
750
+ # cameras such as the CanonPowerShotN100 store ASCII time codes with a
751
+ # leading 2-byte integer giving the length of the string
752
+ # (and chapter names start with a 2-byte integer too)
753
+ if ($size >= 2 and unpack('n',$buff) == $size - 2) {
754
+ next if $size == 2;
755
+ $buff = substr($buff,2);
756
+ }
757
+ my $val;
758
+ # check for encrypted GPS text as written by E-PRANCE B47FS camera
759
+ if ($buff =~ /^\0/ and $buff =~ /\x0a$/ and length($buff) > 5) {
760
+ # decode simple ASCII difference cipher,
761
+ # based on known value of 4th-last char = '*'
762
+ my $dif = ord('*') - ord(substr($buff, -4, 1));
763
+ my $tmp = pack 'C*',map { $_=($_+$dif)&0xff } unpack 'C*',substr $buff,1,-1;
764
+ if ($verbose > 2) {
765
+ $et->VPrint(0, "[decrypted text]\n");
766
+ $et->VerboseDump(\$tmp);
767
+ }
768
+ if ($tmp =~ /^(.*?)(\$[A-Z]{2}RMC.*)/s) {
769
+ ($val, $buff) = ($1, $2);
770
+ $val =~ tr/\t/ /;
771
+ $et->HandleTag($tagTbl, RawGSensor => $val) if length $val;
772
+ }
773
+ } elsif ($buff =~ /^PNDM/ and length $buff >= 20) {
774
+ # Garmin Dashcam format (actually binary, not text)
775
+ $et->HandleTag($tagTbl, GPSLatitude => Get32s(\$buff, 12) * 180/0x80000000);
776
+ $et->HandleTag($tagTbl, GPSLongitude => Get32s(\$buff, 16) * 180/0x80000000);
777
+ $et->HandleTag($tagTbl, GPSSpeed => Get16u(\$buff, 8));
778
+ $et->HandleTag($tagTbl, GPSSpeedRef => 'M');
779
+ next;
780
+ }
781
+ unless (defined $val) {
782
+ $et->HandleTag($tagTbl, Text => $buff); # just store any other text
783
+ next;
784
+ }
785
+ }
786
+ ParseText($et, $tagTbl, \$buff);
787
+
788
+ } elsif ($processByMetaFormat{$type}) {
789
+
790
+ if ($$tagTbl{$metaFormat}) {
791
+ my $tagInfo = $et->GetTagInfo($tagTbl, $metaFormat, \$buff);
792
+ if ($tagInfo) {
793
+ FoundSomething($et, $tagTbl, $time[$i], $dur[$i]);
794
+ $$et{ee} = $ee; # need ee information for 'keys'
795
+ $et->HandleTag($tagTbl, $metaFormat, undef,
796
+ DataPt => \$buff,
797
+ Base => $$start[$i],
798
+ TagInfo => $tagInfo,
799
+ );
800
+ delete $$et{ee};
801
+ } elsif ($metaFormat eq 'camm' and $buff =~ /^X/) {
802
+ # seen 'camm' metadata in this format (X/Y/Z acceleration and G force? + GPRMC + ?)
803
+ # "X0000.0000Y0000.0000Z0000.0000G0000.0000$GPRMC,000125,V,,,,,000.0,,280908,002.1,N*71~, 794021 \x0a"
804
+ FoundSomething($et, $tagTbl, $time[$i], $dur[$i]);
805
+ $et->HandleTag($tagTbl, Accelerometer => "$1 $2 $3 $4") if $buff =~ /X(.*?)Y(.*?)Z(.*?)G(.*?)\$/;
806
+ ParseText($et, $tagTbl, \$buff);
807
+ }
808
+ } elsif ($verbose) {
809
+ $et->VPrint(0, "Unknown meta format ($metaFormat)");
810
+ }
811
+
812
+ } elsif ($type eq 'gps ') { # (ie. GPSDataList tag)
813
+
814
+ if ($buff =~ /^....freeGPS /s) {
815
+ # decode "freeGPS " data (Novatek)
816
+ ProcessFreeGPS($et, {
817
+ DataPt => \$buff,
818
+ SampleTime => $time[$i],
819
+ SampleDuration => $dur[$i],
820
+ }, $tagTbl) ;
821
+ }
822
+
823
+ } elsif ($$tagTbl{$type}) {
824
+
825
+ my $tagInfo = $et->GetTagInfo($tagTbl, $type, \$buff);
826
+ if ($tagInfo) {
827
+ FoundSomething($et, $tagTbl, $time[$i], $dur[$i]);
828
+ $et->HandleTag($tagTbl, $type, undef,
829
+ DataPt => \$buff,
830
+ Base => $$start[$i],
831
+ TagInfo => $tagInfo,
832
+ );
833
+ }
834
+ }
835
+ }
836
+ if ($verbose) {
837
+ $$et{INDENT} = $oldIndent;
838
+ $et->VPrint(0, "--------------------------\n");
839
+ }
840
+ # clean up
841
+ $raf->Seek($tell, 0); # restore original file position
842
+ $$et{DOC_NUM} = 0;
843
+ $$et{HandlerType} = $$et{HanderDesc} = '';
844
+ }
845
+
846
+ #------------------------------------------------------------------------------
847
+ # Process "freeGPS " data blocks referenced by a 'gps ' (GPSDataList) atom
848
+ # Inputs: 0) ExifTool ref, 1) dirInfo ref {DataPt,SampleTime,SampleDuration}, 2) tagTable ref
849
+ # Returns: 1 on success (or 0 on unrecognized or "measurement-void" GPS data)
850
+ # Notes:
851
+ # - also see ProcessFreeGPS2() below for processing of other types of freeGPS blocks
852
+ sub ProcessFreeGPS($$$)
853
+ {
854
+ my ($et, $dirInfo, $tagTbl) = @_;
855
+ my $dataPt = $$dirInfo{DataPt};
856
+ my $dirLen = length $$dataPt;
857
+ my ($yr, $mon, $day, $hr, $min, $sec, $stat, $lbl);
858
+ my ($lat, $latRef, $lon, $lonRef, $spd, $trk, $alt, @acc, @xtra);
859
+
860
+ return 0 if $dirLen < 92;
861
+
862
+ if (substr($$dataPt,12,1) eq "\x05") {
863
+
864
+ # decode encrypted ASCII-based GPS (DashCam Azdome GS63H, ref 5)
865
+ # header looks like this in my sample:
866
+ # 0000: 00 00 80 00 66 72 65 65 47 50 53 20 05 01 00 00 [....freeGPS ....]
867
+ # 0010: 01 03 aa aa f2 e1 f0 ee 54 54 98 9a 9b 92 9a 93 [........TT......]
868
+ # 0020: 98 9e 98 98 9e 93 98 92 a6 9f 9f 9c 9d ed fa 8a [................]
869
+ my $n = $dirLen - 18;
870
+ $n = 0x101 if $n > 0x101;
871
+ my $buf2 = pack 'C*', map { $_ ^ 0xaa } unpack 'C*', substr($$dataPt,18,$n);
872
+ if ($et->Options('Verbose') > 1) {
873
+ $et->VPrint(1, '[decrypted freeGPS data]');
874
+ $et->VerboseDump(\$buf2);
875
+ }
876
+ # (extract longitude as 9 digits, not 8, ref PH)
877
+ 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;
878
+ ($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);
879
+ $spd += 0; # remove leading 0's
880
+ $lbl =~ s/\0.*//s; $lbl =~ s/\s+$//; # truncate at null and remove trailing spaces
881
+ push @xtra, UserLabel => $lbl if length $lbl;
882
+ # extract accelerometer data (ref PH)
883
+ @acc = ($1/100,$2/100,$3/100) if $buf2 =~ /^.{173}([-+]\d{3})([-+]\d{3})([-+]\d{3})/s;
884
+
885
+ } elsif ($$dataPt =~ /^.{52}(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})/s) {
886
+
887
+ # decode NMEA-format GPS data (NextBase 512GW dashcam, ref PH)
888
+ # header looks like this in my sample:
889
+ # 0000: 00 00 80 00 66 72 65 65 47 50 53 20 40 01 00 00 [....freeGPS @...]
890
+ # 0010: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 [................]
891
+ # 0020: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 [................]
892
+ push @xtra, CameraDateTime => "$1:$2:$3 $4:$5:$6";
893
+ 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) {
894
+ ($lat,$latRef,$lon,$lonRef) = ($5,$6,$7,$8);
895
+ $yr = $13 + ($13 >= 70 ? 1900 : 2000);
896
+ ($mon,$day,$hr,$min,$sec) = ($12,$11,$1,$2,$3);
897
+ $spd = $9 * $knotsToKph if length $9;
898
+ $trk = $10 if length $10;
899
+ }
900
+ 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) {
901
+ ($hr,$min,$sec,$lat,$latRef,$lon,$lonRef) = ($1,$2,$3,$5,$6,$7,$8) unless defined $yr;
902
+ $alt = $11;
903
+ unshift @xtra, GPSSatellites => $9;
904
+ unshift @xtra, GPSDOP => $10;
905
+ }
906
+ if (defined $lat) {
907
+ # extract accelerometer readings if GPS was valid
908
+ @acc = unpack('x68V3', $$dataPt);
909
+ # change to signed integer and divide by 256
910
+ map { $_ = $_ - 4294967296 if $_ >= 0x80000000; $_ /= 256 } @acc;
911
+ }
912
+
913
+ } else {
914
+
915
+ # decode binary GPS format (Viofo A119S, ref 2)
916
+ # header looks like this in my sample:
917
+ # 0000: 00 00 80 00 66 72 65 65 47 50 53 20 4c 00 00 00 [....freeGPS L...]
918
+ # 0010: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 [................]
919
+ # 0020: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 [................]
920
+ # (records are same structure as Type 3 Novatek GPS in ProcessFreeGPS2() below)
921
+ ($hr,$min,$sec,$yr,$mon,$day,$stat,$latRef,$lonRef,$lat,$lon,$spd,$trk) =
922
+ unpack('x48V6a1a1a1x1V4', $$dataPt);
923
+ # ignore invalid fixes
924
+ return 0 unless $stat eq 'A' and ($latRef eq 'N' or $latRef eq 'S') and
925
+ ($lonRef eq 'E' or $lonRef eq 'W');
926
+ ($lat,$lon,$spd,$trk) = unpack 'f*', pack 'L*', $lat, $lon, $spd, $trk;
927
+ $yr += 2000 if $yr < 2000;
928
+ $spd *= $knotsToKph; # convert speed to km/h
929
+ # ($trk is not confirmed; may be GPSImageDirection, ref PH)
930
+ }
931
+ #
932
+ # save tag values extracted by above code
933
+ #
934
+ FoundSomething($et, $tagTbl, $$dirInfo{SampleTime}, $$dirInfo{SampleDuration});
935
+ # lat/long are in DDDMM.MMMM format
936
+ my $deg = int($lat / 100);
937
+ $lat = $deg + ($lat - $deg * 100) / 60;
938
+ $deg = int($lon / 100);
939
+ $lon = $deg + ($lon - $deg * 100) / 60;
940
+ $sec = '0' . $sec unless $sec =~ /^\d{2}/; # pad integer part of seconds to 2 digits
941
+ if (defined $yr) {
942
+ my $time = sprintf('%.4d:%.2d:%.2d %.2d:%.2d:%sZ',$yr,$mon,$day,$hr,$min,$sec);
943
+ $et->HandleTag($tagTbl, GPSDateTime => $time);
944
+ } elsif (defined $hr) {
945
+ my $time = sprintf('%.2d:%.2d:%sZ',$hr,$min,$sec);
946
+ $et->HandleTag($tagTbl, GPSTimeStamp => $time);
947
+ }
948
+ $et->HandleTag($tagTbl, GPSLatitude => $lat * ($latRef eq 'S' ? -1 : 1));
949
+ $et->HandleTag($tagTbl, GPSLongitude => $lon * ($lonRef eq 'W' ? -1 : 1));
950
+ $et->HandleTag($tagTbl, GPSAltitude => $alt) if defined $alt;
951
+ if (defined $spd) {
952
+ $et->HandleTag($tagTbl, GPSSpeed => $spd);
953
+ $et->HandleTag($tagTbl, GPSSpeedRef => 'K');
954
+ }
955
+ if (defined $trk) {
956
+ $et->HandleTag($tagTbl, GPSTrack => $trk);
957
+ $et->HandleTag($tagTbl, GPSTrackRef => 'T');
958
+ }
959
+ while (@xtra) {
960
+ my $tag = shift @xtra;
961
+ $et->HandleTag($tagTbl, $tag => shift @xtra);
962
+ }
963
+ $et->HandleTag($tagTbl, Accelerometer => \@acc) if @acc;
964
+ return 1;
965
+ }
966
+
967
+ #------------------------------------------------------------------------------
968
+ # Process "freeGPS " data blocks _not_ referenced by a 'gps ' atom
969
+ # Inputs: 0) ExifTool ref, 1) dirInfo ref {DataPt,DataPos,DirLen}, 2) tagTable ref
970
+ # Returns: 1 on success
971
+ # Notes:
972
+ # - also see ProcessFreeGPS() above
973
+ sub ProcessFreeGPS2($$$)
974
+ {
975
+ my ($et, $dirInfo, $tagTbl) = @_;
976
+ my $dataPt = $$dirInfo{DataPt};
977
+ my $dirLen = $$dirInfo{DirLen};
978
+ my ($yr, $mon, $day, $hr, $min, $sec, $pos);
979
+ my ($lat, $latRef, $lon, $lonRef, $spd, $trk, $alt, $ddd, $unk);
980
+
981
+ return 0 if $dirLen < 82; # minimum size of block with a single GPS record
982
+
983
+ if (substr($$dataPt,0x45,3) eq 'ATC') {
984
+
985
+ # header looks like this: (sample 1)
986
+ # 0000: 00 00 80 00 66 72 65 65 47 50 53 20 38 06 00 00 [....freeGPS 8...]
987
+ # 0010: 49 51 53 32 30 31 33 30 33 30 36 42 00 00 00 00 [IQS20130306B....]
988
+ # 0020: 4d 61 79 20 31 35 20 32 30 31 35 2c 20 31 39 3a [May 15 2015, 19:]
989
+ # (sample 2)
990
+ # 0000: 00 00 80 00 66 72 65 65 47 50 53 20 4c 06 00 00 [....freeGPS L...]
991
+ # 0010: 32 30 31 33 30 33 31 38 2e 30 31 00 00 00 00 00 [20130318.01.....]
992
+ # 0020: 4d 61 72 20 31 38 20 32 30 31 33 2c 20 31 34 3a [Mar 18 2013, 14:]
993
+
994
+ my ($recPos, $lastRecPos, $foundNew);
995
+ my $verbose = $et->Options('Verbose');
996
+ my $dataPos = $$dirInfo{DataPos};
997
+ my $then = $$et{FreeGPS2}{Then};
998
+ $then or $then = $$et{FreeGPS2}{Then} = [ (0) x 6 ];
999
+ # Loop through records in the ATC-type GPS block until we find the most recent.
1000
+ # If we have already found one, then we only need to check the first record
1001
+ # (in case the buffer wrapped around), and the record after the position of
1002
+ # the last record we found, because the others will be old. Odd, but this
1003
+ # is the way it is done... I have only seen one new 52-byte record in the
1004
+ # entire 32 kB block, but the entire device ring buffer (containing 30
1005
+ # entries in my samples) is stored every time. The code below allows for
1006
+ # the possibility of missing blocks and multiple new records in a single
1007
+ # block, but I have never seen this. Note that there may be some earlier
1008
+ # GPS records at the end of the first block that we will miss decoding, but
1009
+ # these should (I believe) be before the start of the video
1010
+ ATCRec: for ($recPos = 0x30; $recPos + 52 < $dirLen; $recPos += 52) {
1011
+
1012
+ my $a = substr($$dataPt, $recPos, 52); # isolate a single record
1013
+ # decrypt record
1014
+ my @a = unpack('C*', $a);
1015
+ my ($key1, $key2) = @a[0x14, 0x1c];
1016
+ $a[$_] ^= $key1 foreach 0x00..0x14, 0x18..0x1b;
1017
+ $a[$_] ^= $key2 foreach 0x1c, 0x20..0x32;
1018
+ my $b = pack 'C*', @a;
1019
+ # unpack and validate date/time
1020
+ my @now = unpack 'x13C3x28vC2', $b; # (H-1,M,S,Y,m,d)
1021
+ $now[0] = ($now[0] + 1) & 0xff; # increment hour
1022
+ my $i;
1023
+ for ($i=0; $i<@dateMax; ++$i) {
1024
+ next if $now[$i] <= $dateMax[$i];
1025
+ $et->WarnOnce('Invalid GPS date/time');
1026
+ next ATCRec; # ignore this record
1027
+ }
1028
+ # look for next ATC record in temporal sequence
1029
+ foreach $i (3..5, 0..2) {
1030
+ if ($now[$i] < $$then[$i]) {
1031
+ last ATCRec if $foundNew;
1032
+ last;
1033
+ }
1034
+ next if $now[$i] == $$then[$i];
1035
+ # we found a more recent record -- extract it and remember its location
1036
+ if ($verbose) {
1037
+ $et->VPrint(2, " + [encrypted GPS record]\n");
1038
+ $et->VerboseDump(\$a, DataPos => $dataPos + $recPos);
1039
+ $et->VPrint(2, " + [decrypted GPS record]\n");
1040
+ $et->VerboseDump(\$b);
1041
+ #my @v = unpack 'H8VVC4V!CA3V!CA3VvvV!vCCCCH4', $b;
1042
+ #$et->VPrint(2, " + [unpacked: @v]\n");
1043
+ # values unpacked above (ref PH):
1044
+ # 0) 0x00 4 bytes - byte 0=1, 1=counts to 255, 2=record index, 3=0 (ref 3)
1045
+ # 1) 0x04 4 bytes - int32u: bits 0-4=day, 5-8=mon, 9-19=year (ref 3)
1046
+ # 2) 0x08 4 bytes - int32u: bits 0-5=sec, 6-11=min, 12-16=hour (ref 3)
1047
+ # 3) 0x0c 1 byte - seen values of 0,1,2 - GPS status maybe?
1048
+ # 4) 0x0d 1 byte - hour minus 1
1049
+ # 5) 0x0e 1 byte - minute
1050
+ # 6) 0x0f 1 byte - second
1051
+ # 7) 0x10 4 bytes - int32s latitude * 1e7
1052
+ # 8) 0x14 1 byte - always 0 (used for decryption)
1053
+ # 9) 0x15 3 bytes - always "ATC"
1054
+ # 10) 0x18 4 bytes - int32s longitude * 1e7
1055
+ # 11) 0x1c 1 byte - always 0 (used for decryption)
1056
+ # 12) 0x1d 3 bytes - always "001"
1057
+ # 13) 0x20 4 bytes - int32s speed * 100 (m/s)
1058
+ # 14) 0x24 2 bytes - int16u heading * 100 (-180 to 180 deg)
1059
+ # 15) 0x26 2 bytes - always zero
1060
+ # 16) 0x28 4 bytes - int32s altitude * 1000 (ref 3)
1061
+ # 17) 0x2c 2 bytes - int16u year
1062
+ # 18) 0x2e 1 byte - month
1063
+ # 19) 0x2f 1 byte - day
1064
+ # 20) 0x30 1 byte - unknown
1065
+ # 21) 0x31 1 byte - always zero
1066
+ # 22) 0x32 2 bytes - checksum ?
1067
+ }
1068
+ @$then = @now;
1069
+ $$et{DOC_NUM} = ++$$et{DOC_COUNT};
1070
+ $trk = Get16s(\$b, 0x24) / 100;
1071
+ $trk += 360 if $trk < 0;
1072
+ my $time = sprintf('%.4d:%.2d:%.2d %.2d:%.2d:%.2dZ', @now[3..5, 0..2]);
1073
+ $et->HandleTag($tagTbl, GPSDateTime => $time);
1074
+ $et->HandleTag($tagTbl, GPSLatitude => Get32s(\$b, 0x10) / 1e7);
1075
+ $et->HandleTag($tagTbl, GPSLongitude => Get32s(\$b, 0x18) / 1e7);
1076
+ $et->HandleTag($tagTbl, GPSSpeed => Get32s(\$b, 0x20) / 100 * $mpsToKph);
1077
+ $et->HandleTag($tagTbl, GPSSpeedRef => 'K');
1078
+ $et->HandleTag($tagTbl, GPSTrack => $trk);
1079
+ $et->HandleTag($tagTbl, GPSTrackRef => 'T');
1080
+ $et->HandleTag($tagTbl, GPSAltitude => Get32s(\$b, 0x28) / 1000);
1081
+ $lastRecPos = $recPos;
1082
+ $foundNew = 1;
1083
+ # don't skip to location of previous recent record in ring buffer
1084
+ # since we found a more recent record here
1085
+ delete $$et{FreeGPS2}{RecentRecPos};
1086
+ last;
1087
+ }
1088
+ # skip older records
1089
+ my $recentRecPos = $$et{FreeGPS2}{RecentRecPos};
1090
+ $recPos = $recentRecPos if $recentRecPos and $recPos < $recentRecPos;
1091
+ }
1092
+ # save position of most recent record (needed when parsing the next freeGPS block)
1093
+ $$et{FreeGPS2}{RecentRecPos} = $lastRecPos;
1094
+ return 1;
1095
+
1096
+ } elsif ($$dataPt =~ /^.{60}A\0.{10}([NS])\0.{14}([EW])\0/s) {
1097
+
1098
+ # header looks like this in my sample:
1099
+ # 0000: 00 00 80 00 66 72 65 65 47 50 53 20 08 01 00 00 [....freeGPS ....]
1100
+ # 0010: 32 30 31 33 30 38 31 35 2e 30 31 00 00 00 00 00 [20130815.01.....]
1101
+ # 0020: 4a 75 6e 20 31 30 20 32 30 31 37 2c 20 31 34 3a [Jun 10 2017, 14:]
1102
+
1103
+ # Type 2 (ref PH):
1104
+ # 0x30 - int32u hour
1105
+ # 0x34 - int32u minute
1106
+ # 0x38 - int32u second
1107
+ # 0x3c - int32u GPS status ('A' or 'V')
1108
+ # 0x40 - double latitude (DDMM.MMMMMM)
1109
+ # 0x48 - int32u latitude ref ('N' or 'S')
1110
+ # 0x50 - double longitude (DDMM.MMMMMM)
1111
+ # 0x58 - int32u longitude ref ('E' or 'W')
1112
+ # 0x60 - double speed (knots)
1113
+ # 0x68 - double heading (deg)
1114
+ # 0x70 - int32u year - 2000
1115
+ # 0x74 - int32u month
1116
+ # 0x78 - int32u day
1117
+ ($latRef, $lonRef) = ($1, $2);
1118
+ ($hr,$min,$sec,$yr,$mon,$day) = unpack('x48V3x52V3', $$dataPt);
1119
+ $lat = GetDouble($dataPt, 0x40);
1120
+ $lon = GetDouble($dataPt, 0x50);
1121
+ $spd = GetDouble($dataPt, 0x60) * $knotsToKph;
1122
+ $trk = GetDouble($dataPt, 0x68);
1123
+
1124
+ } elsif ($$dataPt =~ /^.{72}A([NS])([EW])/s) {
1125
+
1126
+ # Type 3 (Novatek GPS, ref 2): (in case it wasn't decoded via 'gps ' atom)
1127
+ # 0x30 - int32u hour
1128
+ # 0x34 - int32u minute
1129
+ # 0x38 - int32u second
1130
+ # 0x3c - int32u year - 2000
1131
+ # 0x40 - int32u month
1132
+ # 0x44 - int32u day
1133
+ # 0x48 - int8u GPS status ('A' or 'V')
1134
+ # 0x49 - int8u latitude ref ('N' or 'S')
1135
+ # 0x4a - int8u longitude ref ('E' or 'W')
1136
+ # 0x4b - 0
1137
+ # 0x4c - float latitude (DDMM.MMMMMM)
1138
+ # 0x50 - float longitude (DDMM.MMMMMM)
1139
+ # 0x54 - float speed (knots)
1140
+ # 0x58 - float heading (deg)
1141
+ # Type 3b, same as above for 0x30-0x4a (ref PH)
1142
+ # 0x4c - int32s latitude (decimal degrees * 1e7)
1143
+ # 0x50 - int32s longitude (decimal degrees * 1e7)
1144
+ # 0x54 - int32s speed (m/s * 100)
1145
+ # 0x58 - float altitude (m * 1000, NC)
1146
+ ($latRef, $lonRef) = ($1, $2);
1147
+ ($hr,$min,$sec,$yr,$mon,$day) = unpack('x48V6', $$dataPt);
1148
+ if (substr($$dataPt, 16, 3) eq 'IQS') {
1149
+ # Type 3b (ref PH)
1150
+ # header looks like this in my sample:
1151
+ # 0000: 00 00 80 00 66 72 65 65 47 50 53 20 4c 00 00 00 [....freeGPS L...]
1152
+ # 0010: 49 51 53 5f 41 37 5f 32 30 31 35 30 34 31 37 00 [IQS_A7_20150417.]
1153
+ # 0020: 4d 61 72 20 32 39 20 32 30 31 37 2c 20 31 36 3a [Mar 29 2017, 16:]
1154
+ $ddd = 1;
1155
+ $lat = abs Get32s($dataPt, 0x4c) / 1e7;
1156
+ $lon = abs Get32s($dataPt, 0x50) / 1e7;
1157
+ $spd = Get32s($dataPt, 0x54) / 100 * $mpsToKph;
1158
+ $alt = GetFloat($dataPt, 0x58) / 1000; # (NC)
1159
+ } else {
1160
+ # Type 3 (ref 2)
1161
+ # (no sample with this format)
1162
+ $lat = GetFloat($dataPt, 0x4c);
1163
+ $lon = GetFloat($dataPt, 0x50);
1164
+ $spd = GetFloat($dataPt, 0x54) * $knotsToKph;
1165
+ $trk = GetFloat($dataPt, 0x58);
1166
+ }
1167
+
1168
+ } else {
1169
+
1170
+ # (look for binary GPS as stored by NextBase 512G, ref PH)
1171
+ # header looks like this in my sample:
1172
+ # 0000: 00 00 80 00 66 72 65 65 47 50 53 20 78 01 00 00 [....freeGPS x...]
1173
+ # 0010: 78 2e 78 78 00 00 00 00 00 00 00 00 00 00 00 00 [x.xx............]
1174
+ # 0020: 30 30 30 30 30 00 00 00 00 00 00 00 00 00 00 00 [00000...........]
1175
+
1176
+ # followed by a number of 32-byte records in this format (big endian!):
1177
+ # 0x30 - int16u unknown (seen: 0x24 0x53 = "$S")
1178
+ # 0x32 - int16u speed (m/s * 100)
1179
+ # 0x34 - int16s heading (deg * 100) (or GPSImgDirection?)
1180
+ # 0x36 - int16u year
1181
+ # 0x38 - int8u month
1182
+ # 0x39 - int8u day
1183
+ # 0x3a - int8u hour
1184
+ # 0x3b - int8u min
1185
+ # 0x3c - int16u sec * 10
1186
+ # 0x3e - int8u unknown (seen: 2)
1187
+ # 0x3f - int32s latitude (decimal degrees * 1e7)
1188
+ # 0x43 - int32s longitude (decimal degrees * 1e7)
1189
+ # 0x47 - int8u unknown (seen: 16)
1190
+ # 0x48-0x4f - all zero
1191
+ for ($pos=0x32; ; ) {
1192
+ ($spd,$trk,$yr,$mon,$day,$hr,$min,$sec,$unk,$lat,$lon) = unpack "x${pos}nnnCCCCnCNN", $$dataPt;
1193
+ # validate record using date/time
1194
+ last if $yr < 2000 or $yr > 2200 or
1195
+ $mon < 1 or $mon > 12 or
1196
+ $day < 1 or $day > 31 or
1197
+ $hr > 59 or $min > 59 or $sec > 600;
1198
+ # change lat/lon to signed integer and divide by 1e7
1199
+ map { $_ = $_ - 4294967296 if $_ >= 0x80000000; $_ /= 1e7 } $lat, $lon;
1200
+ $trk -= 0x10000 if $trk >= 0x8000; # make it signed
1201
+ $trk /= 100;
1202
+ $trk += 360 if $trk < 0;
1203
+ my $time = sprintf("%.4d:%.2d:%.2d %.2d:%.2d:%04.1fZ", $yr, $mon, $day, $hr, $min, $sec/10);
1204
+ $$et{DOC_NUM} = ++$$et{DOC_COUNT};
1205
+ $et->HandleTag($tagTbl, GPSDateTime => $time);
1206
+ $et->HandleTag($tagTbl, GPSLatitude => $lat);
1207
+ $et->HandleTag($tagTbl, GPSLongitude => $lon);
1208
+ $et->HandleTag($tagTbl, GPSSpeed => $spd / 100 * $mpsToKph);
1209
+ $et->HandleTag($tagTbl, GPSSpeedRef => 'K');
1210
+ $et->HandleTag($tagTbl, GPSTrack => $trk);
1211
+ $et->HandleTag($tagTbl, GPSTrackRef => 'T');
1212
+ last if $pos += 0x20 > length($$dataPt) - 0x1e;
1213
+ }
1214
+ return $$et{DOC_NUM} ? 1 : 0; # return 0 if nothing extracted
1215
+ }
1216
+ #
1217
+ # save tag values extracted by above code
1218
+ #
1219
+ return 0 if $mon < 1 or $mon > 12; # quick sanity check
1220
+ $$et{DOC_NUM} = ++$$et{DOC_COUNT};
1221
+ $yr += 2000 if $yr < 2000;
1222
+ my $time = sprintf('%.4d:%.2d:%.2d %.2d:%.2d:%.2dZ', $yr, $mon, $day, $hr, $min, $sec);
1223
+ # convert from DDMM.MMMMMM to DD.DDDDDD format if necessary
1224
+ unless ($ddd) {
1225
+ my $deg = int($lat / 100);
1226
+ $lat = $deg + ($lat - $deg * 100) / 60;
1227
+ $deg = int($lon / 100);
1228
+ $lon = $deg + ($lon - $deg * 100) / 60;
1229
+ }
1230
+ $et->HandleTag($tagTbl, GPSDateTime => $time);
1231
+ $et->HandleTag($tagTbl, GPSLatitude => $lat * ($latRef eq 'S' ? -1 : 1));
1232
+ $et->HandleTag($tagTbl, GPSLongitude => $lon * ($lonRef eq 'W' ? -1 : 1));
1233
+ $et->HandleTag($tagTbl, GPSSpeed => $spd); # (now in km/h)
1234
+ $et->HandleTag($tagTbl, GPSSpeedRef => 'K');
1235
+ if (defined $trk) {
1236
+ $et->HandleTag($tagTbl, GPSTrack => $trk);
1237
+ $et->HandleTag($tagTbl, GPSTrackRef => 'T');
1238
+ }
1239
+ if (defined $alt) {
1240
+ $et->HandleTag($tagTbl, GPSAltitude => $alt);
1241
+ }
1242
+ return 1;
1243
+ }
1244
+
1245
+ #------------------------------------------------------------------------------
1246
+ # Extract embedded information referenced from a track
1247
+ # Inputs: 0) ExifTool ref, 1) tag name, 2) data ref
1248
+ sub ParseTag($$$)
1249
+ {
1250
+ local $_;
1251
+ my ($et, $tag, $dataPt) = @_;
1252
+ my $dataLen = length $$dataPt;
1253
+
1254
+ if ($tag eq 'stsz' or $tag eq 'stz2' and $dataLen > 12) {
1255
+ # read the sample sizes
1256
+ my ($sz, $num) = unpack('x4N2', $$dataPt);
1257
+ my $size = $$et{ee}{size} = [ ];
1258
+ if ($tag eq 'stsz') {
1259
+ if ($sz == 0) {
1260
+ @$size = ReadValue($dataPt, 12, 'int32u', $num, $dataLen-12);
1261
+ } else {
1262
+ @$size = ($sz) x $num;
1263
+ }
1264
+ } else {
1265
+ $sz &= 0xff;
1266
+ if ($sz == 4) {
1267
+ my @tmp = ReadValue($dataPt, 12, 'int8u', int(($num+1)/2), $dataLen-12);
1268
+ foreach (@tmp) {
1269
+ push @$size, $_ >> 4;
1270
+ push @$size, $_ & 0xff;
1271
+ }
1272
+ } elsif ($sz == 8 || $sz == 16) {
1273
+ @$size = ReadValue($dataPt, 12, "int${sz}u", $num, $dataLen-12);
1274
+ }
1275
+ }
1276
+ } elsif ($tag eq 'stco' or $tag eq 'co64' and $dataLen > 8) {
1277
+ # read the chunk offsets
1278
+ my $num = unpack('x4N', $$dataPt);
1279
+ my $stco = $$et{ee}{stco} = [ ];
1280
+ @$stco = ReadValue($dataPt, 8, $tag eq 'stco' ? 'int32u' : 'int64u', $num, $dataLen-8);
1281
+ } elsif ($tag eq 'stsc' and $dataLen > 8) {
1282
+ # read the sample-to-chunk box
1283
+ my $num = unpack('x4N', $$dataPt);
1284
+ if ($dataLen >= 8 + $num * 12) {
1285
+ my ($i, @stsc);
1286
+ for ($i=0; $i<$num; ++$i) {
1287
+ # list of (first-chunk, samples-per-chunk, sample-description-index)
1288
+ push @stsc, [ unpack('x'.(8+$i*12).'N3', $$dataPt) ];
1289
+ }
1290
+ $$et{ee}{stsc} = \@stsc;
1291
+ }
1292
+ } elsif ($tag eq 'stts' and $dataLen > 8) {
1293
+ # read the time-to-sample box
1294
+ my $num = unpack('x4N', $$dataPt);
1295
+ if ($dataLen >= 8 + $num * 8) {
1296
+ $$et{ee}{stts} = [ unpack('x8N'.($num*2), $$dataPt) ];
1297
+ }
1298
+ } elsif ($tag eq 'avcC') {
1299
+ # read the AVC compressor configuration
1300
+ $$et{ee}{avcC} = $$dataPt if $dataLen >= 7; # (minimum length is 7)
1301
+ } elsif ($tag eq 'JPEG') {
1302
+ $$et{ee}{JPEG} = $$dataPt;
1303
+ } elsif ($tag eq 'gps ' and $dataLen > 8) {
1304
+ # decode Novatek 'gps ' box (ref 2)
1305
+ my $num = Get32u($dataPt, 4);
1306
+ $num = int(($dataLen - 8) / 8) if $num * 8 + 8 > $dataLen;
1307
+ my $start = $$et{ee}{start} = [ ];
1308
+ my $size = $$et{ee}{size} = [ ];
1309
+ my $i;
1310
+ for ($i=0; $i<$num; ++$i) {
1311
+ push @$start, Get32u($dataPt, 8 + $i * 8);
1312
+ push @$size, Get32u($dataPt, 12 + $i * 8);
1313
+ }
1314
+ $$et{HandlerType} = $tag; # fake handler type
1315
+ ProcessSamples($et); # we have all we need to process sample data now
1316
+ }
1317
+ }
1318
+
1319
+ #------------------------------------------------------------------------------
1320
+ # Process Yuneec 'tx3g' sbtl metadata (ref PH)
1321
+ # Inputs: 0) ExifTool object ref, 1) dirInfo ref, 2) tag table ref
1322
+ # Returns: 1 on success
1323
+ sub Process_tx3g($$$)
1324
+ {
1325
+ my ($et, $dirInfo, $tagTablePtr) = @_;
1326
+ my $dataPt = $$dirInfo{DataPt};
1327
+ return 0 if length $$dataPt < 2;
1328
+ pos($$dataPt) = 2; # skip 2-byte length word
1329
+ $et->HandleTag($tagTablePtr, $1, $2) while $$dataPt =~ /(\w+):([^:]*[^:\s])(\s|$)/sg;
1330
+ return 1;
1331
+ }
1332
+
1333
+ #------------------------------------------------------------------------------
1334
+ # Process QuickTime 'mebx' timed metadata
1335
+ # Inputs: 0) ExifTool ref, 1) dirInfo ref, 2) tag table ref
1336
+ # Returns: 1 on success
1337
+ # - uses tag ID keys stored in the ExifTool ee data member by a previous call to SaveMetaKeys
1338
+ sub Process_mebx($$$)
1339
+ {
1340
+ my ($et, $dirInfo, $tagTbl) = @_;
1341
+ my $ee = $$et{ee} or return 0;
1342
+ return 0 unless $$ee{'keys'};
1343
+ my $dataPt = $$dirInfo{DataPt};
1344
+
1345
+ # parse using information from 'keys' table (eg. Apple iPhone7+ hevc 'Core Media Data Handler')
1346
+ $et->VerboseDir('mebx', undef, length $$dataPt);
1347
+ my $pos = 0;
1348
+ while ($pos + 8 < length $$dataPt) {
1349
+ my $len = Get32u($dataPt, $pos);
1350
+ last if $len < 8 or $pos + $len > length $$dataPt;
1351
+ my $id = substr($$dataPt, $pos+4, 4);
1352
+ my $info = $$ee{'keys'}{$id};
1353
+ if ($info) {
1354
+ my $tag = $$info{TagID};
1355
+ unless ($$tagTbl{$tag}) {
1356
+ next unless $tag =~ /^[-\w.]+$/;
1357
+ # create info for tags with reasonable id's
1358
+ my $name = $tag;
1359
+ $name =~ s/[-.](.)/\U$1/g;
1360
+ AddTagToTable($tagTbl, $tag, { Name => ucfirst($name) });
1361
+ }
1362
+ my $val = ReadValue($dataPt, $pos+8, $$info{Format}, undef, $len-8);
1363
+ $et->HandleTag($tagTbl, $tag, $val,
1364
+ DataPt => $dataPt,
1365
+ Base => $$dirInfo{Base},
1366
+ Start => $pos + 8,
1367
+ Size => $len - 8,
1368
+ );
1369
+ } else {
1370
+ $et->WarnOnce('No key information for mebx ID ' . PrintableTagID($id,1));
1371
+ }
1372
+ $pos += $len;
1373
+ }
1374
+ return 1;
1375
+ }
1376
+
1377
+ #------------------------------------------------------------------------------
1378
+ # Process QuickTime '3gf' timed metadata (Pittasoft Blackvue dashcam)
1379
+ # Inputs: 0) ExifTool ref, 1) dirInfo ref, 2) tag table ref
1380
+ # Returns: 1 on success
1381
+ sub Process_3gf($$$)
1382
+ {
1383
+ my ($et, $dirInfo, $tagTbl) = @_;
1384
+ my $dataPt = $$dirInfo{DataPt};
1385
+ my $dirLen = $$dirInfo{DirLen};
1386
+ my $recLen = 10; # 10-byte record length
1387
+ $et->VerboseDir('3gf', undef, $dirLen);
1388
+ if ($dirLen > $recLen and not $et->Options('ExtractEmbedded')) {
1389
+ $dirLen = $recLen;
1390
+ EEWarn($et);
1391
+ }
1392
+ my $pos;
1393
+ for ($pos=0; $pos+$recLen<=$dirLen; $pos+=$recLen) {
1394
+ $$et{DOC_NUM} = ++$$et{DOC_COUNT};
1395
+ my $tc = Get32u($dataPt, $pos);
1396
+ last if $tc == 0xffffffff;
1397
+ my ($x, $y, $z) = (Get16s($dataPt, $pos+4)/10, Get16s($dataPt, $pos+6)/10, Get16s($dataPt, $pos+8)/10);
1398
+ $et->HandleTag($tagTbl, TimeCode => $tc / 1000);
1399
+ $et->HandleTag($tagTbl, Accelerometer => "$x $y $z");
1400
+ }
1401
+ delete $$et{DOC_NUM};
1402
+ return 1;
1403
+ }
1404
+
1405
+ #------------------------------------------------------------------------------
1406
+ # Process DuDuBell M1 dashcam / VSYS M6L 'gsen' atom (ref PH)
1407
+ # Inputs: 0) ExifTool ref, 1) dirInfo ref, 2) tag table ref
1408
+ # Returns: 1 on success
1409
+ sub Process_gsen($$$)
1410
+ {
1411
+ my ($et, $dirInfo, $tagTbl) = @_;
1412
+ my $dataPt = $$dirInfo{DataPt};
1413
+ my $dirLen = $$dirInfo{DirLen};
1414
+ my $recLen = 3; # 3-byte record length
1415
+ $et->VerboseDir('gsen', undef, $dirLen);
1416
+ if ($dirLen > $recLen and not $et->Options('ExtractEmbedded')) {
1417
+ $dirLen = $recLen;
1418
+ EEWarn($et);
1419
+ }
1420
+ my $pos;
1421
+ for ($pos=0; $pos+$recLen<=$dirLen; $pos+=$recLen) {
1422
+ $$et{DOC_NUM} = ++$$et{DOC_COUNT};
1423
+ my @acc = map { $_ /= 16 } unpack "x${pos}c3", $$dataPt;
1424
+ $et->HandleTag($tagTbl, Accelerometer => "@acc");
1425
+ # (there are no associated timestamps, but these are sampled at 5 Hz in my test video)
1426
+ }
1427
+ delete $$et{DOC_NUM};
1428
+ return 1;
1429
+ }
1430
+
1431
+ #------------------------------------------------------------------------------
1432
+ # Process DuDuBell M1 dashcam / VSYS M6L 'gps0' atom (ref PH)
1433
+ # Inputs: 0) ExifTool ref, 1) dirInfo ref, 2) tag table ref
1434
+ # Returns: 1 on success
1435
+ sub Process_gps0($$$)
1436
+ {
1437
+ my ($et, $dirInfo, $tagTbl) = @_;
1438
+ my $dataPt = $$dirInfo{DataPt};
1439
+ my $dirLen = $$dirInfo{DirLen};
1440
+ my $recLen = 32; # 32-byte record length
1441
+ $et->VerboseDir('gps0', undef, $dirLen);
1442
+ SetByteOrder('II');
1443
+ if ($dirLen > $recLen and not $et->Options('ExtractEmbedded')) {
1444
+ $dirLen = $recLen;
1445
+ EEWarn($et);
1446
+ }
1447
+ my $pos;
1448
+ for ($pos=0; $pos+$recLen<=$dirLen; $pos+=$recLen) {
1449
+ $$et{DOC_NUM} = ++$$et{DOC_COUNT};
1450
+ # lat/long are in DDDMM.MMMM format
1451
+ my $lat = GetDouble($dataPt, $pos);
1452
+ my $lon = GetDouble($dataPt, $pos+8);
1453
+ next if abs($lat) > 9000 or abs($lon) > 18000;
1454
+ # (note: this method works fine for negative coordinates)
1455
+ my $deg = int($lat / 100);
1456
+ $lat = $deg + ($lat - $deg * 100) / 60;
1457
+ $deg = int($lon / 100);
1458
+ $lon = $deg + ($lon - $deg * 100) / 60;
1459
+ my @a = unpack('C*', substr($$dataPt,$pos+22, 6)); # unpack date/time
1460
+ $a[0] += 2000;
1461
+ $et->HandleTag($tagTbl, GPSDateTime => sprintf("%.4d:%.2d:%.2d %.2d:%.2d:%.2dZ", @a));
1462
+ $et->HandleTag($tagTbl, GPSLatitude => $lat);
1463
+ $et->HandleTag($tagTbl, GPSLongitude => $lon);
1464
+ $et->HandleTag($tagTbl, GPSSpeed => Get16u($dataPt, $pos+0x14));
1465
+ $et->HandleTag($tagTbl, GPSSpeedRef => 'K');
1466
+ $et->HandleTag($tagTbl, GPSTrack => Get8u($dataPt, $pos+0x1c) * 2); # (NC)
1467
+ $et->HandleTag($tagTbl, GPSTrackRef => 'T');
1468
+ $et->HandleTag($tagTbl, GPSAltitude => Get32s($dataPt, $pos + 0x10));
1469
+ # yet to be decoded:
1470
+ # 0x1d - int8u[3] seen: "1 1 0"
1471
+ }
1472
+ delete $$et{DOC_NUM};
1473
+ SetByteOrder('MM');
1474
+ return 1;
1475
+ }
1476
+
1477
+ #------------------------------------------------------------------------------
1478
+ # Process 'gps ' atom containing NMEA from Pittasoft Blackvue dashcam (ref PH)
1479
+ # Inputs: 0) ExifTool object ref, 1) dirInfo ref, 2) tag table ref
1480
+ # Returns: 1 on success
1481
+ sub ProcessNMEA($$$)
1482
+ {
1483
+ my ($et, $dirInfo, $tagTbl) = @_;
1484
+ my $dataPt = $$dirInfo{DataPt};
1485
+ # parse only RMC sentence (with leading timestamp) for now
1486
+ while ($$dataPt =~ /(?:\[(\d+)\])?\$[A-Z]{2}RMC,(\d{2})(\d{2})(\d+(\.\d*)?),A?,(\d+\.\d+),([NS]),(\d+\.\d+),([EW]),(\d*\.?\d*),(\d*\.?\d*),(\d{2})(\d{2})(\d+)/g) {
1487
+ my $tc = $1; # milliseconds since 1970 (local time)
1488
+ my ($lat,$latRef,$lon,$lonRef) = ($6,$7,$8,$9);
1489
+ my $yr = $14 + ($14 >= 70 ? 1900 : 2000);
1490
+ my ($mon,$day,$hr,$min,$sec) = ($13,$12,$2,$3,$4);
1491
+ my ($spd, $trk);
1492
+ $spd = $10 * $knotsToKph if length $10;
1493
+ $trk = $11 if length $11;
1494
+ # lat/long are in DDDMM.MMMM format
1495
+ my $deg = int($lat / 100);
1496
+ $lat = $deg + ($lat - $deg * 100) / 60;
1497
+ $deg = int($lon / 100);
1498
+ $lon = $deg + ($lon - $deg * 100) / 60;
1499
+ $sec = '0' . $sec unless $sec =~ /^\d{2}/; # pad integer part of seconds to 2 digits
1500
+ my $time = sprintf('%.4d:%.2d:%.2d %.2d:%.2d:%sZ',$yr,$mon,$day,$hr,$min,$sec);
1501
+ my $sampleTime;
1502
+ $sampleTime = ($tc - $$et{StartTime}) / 1000 if $tc and $$et{StartTime};
1503
+ FoundSomething($et, $tagTbl, $sampleTime);
1504
+ $et->HandleTag($tagTbl, GPSDateTime => $time);
1505
+ $et->HandleTag($tagTbl, GPSLatitude => $lat * ($latRef eq 'S' ? -1 : 1));
1506
+ $et->HandleTag($tagTbl, GPSLongitude => $lon * ($lonRef eq 'W' ? -1 : 1));
1507
+ if (defined $spd) {
1508
+ $et->HandleTag($tagTbl, GPSSpeed => $spd);
1509
+ $et->HandleTag($tagTbl, GPSSpeedRef => 'K');
1510
+ }
1511
+ if (defined $trk) {
1512
+ $et->HandleTag($tagTbl, GPSTrack => $trk);
1513
+ $et->HandleTag($tagTbl, GPSTrackRef => 'T');
1514
+ }
1515
+ }
1516
+ delete $$et{DOC_NUM};
1517
+ return 1;
1518
+ }
1519
+
1520
+ #------------------------------------------------------------------------------
1521
+ # Process TomTom Bandit Action Cam TTAD atom (ref PH)
1522
+ # Inputs: 0) ExifTool object ref, 1) dirInfo ref, 2) tag table ref
1523
+ # Returns: 1 on success
1524
+ my %ttLen = ( # lengths of known TomTom records
1525
+ 0 => 12, # angular velocity (NC)
1526
+ 1 => 4, # ?
1527
+ 2 => 12, # ?
1528
+ 3 => 12, # accelerometer (NC)
1529
+ # (haven't seen a record 4 yet)
1530
+ 5 => 92, # GPS
1531
+ 0xff => 4, # timecode
1532
+ );
1533
+ sub ProcessTTAD($$$)
1534
+ {
1535
+ my ($et, $dirInfo, $tagTbl) = @_;
1536
+ my $dataPt = $$dirInfo{DataPt};
1537
+ my $dirLen = $$dirInfo{DirLen};
1538
+ my $pos = 76;
1539
+
1540
+ return 0 if $dirLen < $pos;
1541
+
1542
+ $et->VerboseDir('TTAD', undef, $dirLen);
1543
+ SetByteOrder('II');
1544
+
1545
+ my $eeOpt = $et->Options('ExtractEmbedded');
1546
+ my $unknown = $et->Options('Unknown');
1547
+ my $found = 0;
1548
+ my $sampleTime = 0;
1549
+ my $resync = 1;
1550
+ my $skipped = 0;
1551
+ my $warned;
1552
+
1553
+ while ($pos < $dirLen) {
1554
+ # get next record type
1555
+ my $type = Get8u($dataPt, $pos++);
1556
+ # resync if necessary by skipping data until next timecode record
1557
+ if ($resync and $type != 0xff) {
1558
+ ++$skipped > 0x100 and $et->Warn('Unrecognized or bad TTAD data', 1), last;
1559
+ next;
1560
+ }
1561
+ unless ($ttLen{$type}) {
1562
+ # skip unknown records
1563
+ $et->Warn("Unknown TTAD record type $type",1) unless $warned;
1564
+ $resync = $warned = 1;
1565
+ ++$skipped;
1566
+ next;
1567
+ }
1568
+ last if $pos + $ttLen{$type} > $dirLen;
1569
+ if ($type == 0xff) { # timecode?
1570
+ my $tm = Get32u($dataPt, $pos);
1571
+ # validate timecode if skipping unknown data
1572
+ if ($resync) {
1573
+ if ($tm < $sampleTime or $tm > $sampleTime + 250) {
1574
+ ++$skipped;
1575
+ next;
1576
+ }
1577
+ undef $resync;
1578
+ $skipped = 0;
1579
+ }
1580
+ $pos += $ttLen{$type};
1581
+ $sampleTime = $tm;
1582
+ next;
1583
+ }
1584
+ unless ($eeOpt) {
1585
+ # only extract one of each type without -ee option
1586
+ $found & (1 << $type) and $pos += $ttLen{$type}, next;
1587
+ $found |= (1 << $type);
1588
+ }
1589
+ if ($type == 0 or $type == 3) {
1590
+ # (these are both just educated guesses - PH)
1591
+ FoundSomething($et, $tagTbl, $sampleTime / 1000);
1592
+ my @a = map { Get32s($dataPt,$pos+4*$_) / 1000 } 0..2;
1593
+ $et->HandleTag($tagTbl, ($type ? 'Accelerometer' : 'AngularVelocity') => "@a");
1594
+ } elsif ($type == 5) {
1595
+ # example records unpacked with 'dVddddVddddv*'
1596
+ # datetime ? spd ele lat lon ? trk ? ? ? ? ? ? ? ? ?
1597
+ # 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
1598
+ # 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
1599
+ # 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
1600
+ # (I think "5" may be the number of satellites. seen: 5,6,7 - PH)
1601
+ FoundSomething($et, $tagTbl, $sampleTime / 1000);
1602
+ my $t = GetDouble($dataPt, $pos);
1603
+ $et->HandleTag($tagTbl, GPSDateTime => Image::ExifTool::ConvertUnixTime($t,undef,3).'Z');
1604
+ $et->HandleTag($tagTbl, GPSAltitude => GetDouble($dataPt, $pos+0x14));
1605
+ $et->HandleTag($tagTbl, GPSLatitude => GetDouble($dataPt, $pos+0x1c));
1606
+ $et->HandleTag($tagTbl, GPSLongitude => GetDouble($dataPt, $pos+0x24));
1607
+ $et->HandleTag($tagTbl, GPSSpeed => GetDouble($dataPt, $pos+0x0c) * $mpsToKph);
1608
+ $et->HandleTag($tagTbl, GPSSpeedRef => 'K');
1609
+ $et->HandleTag($tagTbl, GPSTrack => GetDouble($dataPt, $pos+0x30));
1610
+ $et->HandleTag($tagTbl, GPSTrackRef => 'T');
1611
+ if ($unknown) {
1612
+ my @a = map { GetDouble($dataPt, $pos+0x38+8*$_) } 0..2;
1613
+ $et->HandleTag($tagTbl, Unknown03 => "@a");
1614
+ }
1615
+ } elsif ($type < 3) {
1616
+ # as yet unknown:
1617
+ # 1 - int32s[1]? (values around 98k)
1618
+ # 2 - int32s[3] (values like "806 8124 4323" -- probably something * 1000 again)
1619
+ if ($unknown) {
1620
+ FoundSomething($et, $tagTbl, $sampleTime / 1000);
1621
+ my $n = $type == 1 ? 0 : 2;
1622
+ my @a = map { Get32s($dataPt,$pos+4*$_) } 0..$n;
1623
+ $et->HandleTag($tagTbl, "Unknown0$type" => "@a");
1624
+ }
1625
+ } else {
1626
+ $et->WarnOnce("Unknown TTAD record type $type",1);
1627
+ }
1628
+ # without -ee, stop after we find types 0,3,5 (ie. bitmask 0x29)
1629
+ $eeOpt or ($found & 0x29) != 0x29 or EEWarn($et), last;
1630
+ $pos += $ttLen{$type};
1631
+ }
1632
+ SetByteOrder('MM');
1633
+ delete $$et{DOC_NUM};
1634
+ return 1;
1635
+ }
1636
+
1637
+ #------------------------------------------------------------------------------
1638
+ # Extract information from Insta360 trailer
1639
+ # Inputs: 0) ExifTool ref
1640
+ sub ProcessINSVTrailer($)
1641
+ {
1642
+ local $_;
1643
+ my $et = shift;
1644
+ my $raf = $$et{RAF};
1645
+ my $buff;
1646
+
1647
+ return unless $raf->Seek(-78, 2) and $raf->Read($buff, 78) == 78 and
1648
+ substr($buff,-32) eq "8db42d694ccc418790edff439fe026bf"; # check magic number
1649
+
1650
+ my $tagTbl = GetTagTable('Image::ExifTool::QuickTime::Stream');
1651
+ my $fileEnd = $raf->Tell();
1652
+ my $trailerLen = unpack('x38V', $buff);
1653
+ my $trailerStart = $fileEnd - $trailerLen;
1654
+ my $unknown = $et->Options('Unknown');
1655
+ my $verbose = $et->Options('Verbose');
1656
+ my $pos = $fileEnd - 78;
1657
+ my ($i, $p);
1658
+ SetByteOrder('II');
1659
+ # loop through all records in the trailer, from last to first
1660
+ for (;;) {
1661
+ my ($id, $len) = unpack('vV', $buff);
1662
+ ($pos -= $len) < $trailerStart and last;
1663
+ $raf->Seek($pos, 0) or last;
1664
+ $raf->Read($buff, $len) == $len or last;
1665
+ if ($verbose) {
1666
+ $et->VPrint(0, sprintf("INSV Record 0x%x (offset 0x%x, %d bytes):\n", $id, $pos, $len));
1667
+ $et->VerboseDump(\$buff);
1668
+ }
1669
+ my $dlen = $insvDataLen{$id};
1670
+ if ($dlen) {
1671
+ $len % $dlen and $et->Warn(sprintf('Unexpected INSV record 0x%x length',$id)), last;
1672
+ if ($id == 0x300) {
1673
+ for ($p=0; $p<$len; $p+=$dlen) {
1674
+ $$et{DOC_NUM} = ++$$et{DOC_COUNT};
1675
+ my @a = map { GetDouble(\$buff, $p + 8 * $_) } 1..6;
1676
+ $et->HandleTag($tagTbl, TimeCode => sprintf('%.3f', Get64u(\$buff, $p) / 1000));
1677
+ $et->HandleTag($tagTbl, Accelerometer => "@a[0..2]"); # (NC)
1678
+ $et->HandleTag($tagTbl, AngularVelocity => "@a[3..5]"); # (NC)
1679
+ }
1680
+ } elsif ($id == 0x400 and $unknown) {
1681
+ for ($p=0; $p<$len; $p+=$dlen) {
1682
+ $$et{DOC_NUM} = ++$$et{DOC_COUNT};
1683
+ $et->HandleTag($tagTbl, TimeCode => sprintf('%.3f', Get64u(\$buff, $p) / 1000));
1684
+ $et->HandleTag($tagTbl, Unknown01 => GetDouble(\$buff, $p + 8));
1685
+ }
1686
+ } elsif ($id == 0x700) {
1687
+ for ($p=0; $p<$len; $p+=$dlen) {
1688
+ my $tmp = substr($buff, $p, $dlen);
1689
+ my @a = unpack('VVvaa8aa8aa8a8a8', $tmp);
1690
+ next unless $a[3] eq 'A'; # (ignore void fixes)
1691
+ last unless ($a[5] eq 'N' or $a[5] eq 'S') and # (quick validation)
1692
+ ($a[7] eq 'E' or $a[7] eq 'W');
1693
+ $$et{DOC_NUM} = ++$$et{DOC_COUNT};
1694
+ $a[$_] = GetDouble(\$a[$_], 0) foreach 4,6,8,9,10;
1695
+ $a[4] *= -abs($a[4]) if $a[5] eq 'S'; # (abs just in case it was already signed)
1696
+ $a[6] *= -abs($a[6]) if $a[7] eq 'W';
1697
+ $et->HandleTag($tagTbl, GPSDateTime => Image::ExifTool::ConvertUnixTime($a[0]) . 'Z');
1698
+ $et->HandleTag($tagTbl, GPSLatitude => $a[4]);
1699
+ $et->HandleTag($tagTbl, GPSLongitude => $a[6]);
1700
+ $et->HandleTag($tagTbl, GPSSpeed => $a[8] * $mpsToKph);
1701
+ $et->HandleTag($tagTbl, GPSSpeedRef => 'K');
1702
+ $et->HandleTag($tagTbl, GPSTrack => $a[9]);
1703
+ $et->HandleTag($tagTbl, GPSTrackRef => 'T');
1704
+ $et->HandleTag($tagTbl, GPSAltitude => $a[10]);
1705
+ $et->HandleTag($tagTbl, Unknown02 => "@a[1,2]");
1706
+ }
1707
+ }
1708
+ } elsif ($id == 0x101) {
1709
+ my $tagTablePtr = GetTagTable('Image::ExifTool::QuickTime::INSV_MakerNotes');
1710
+ for ($i=0, $p=0; $i<4; ++$i) {
1711
+ last if $p + 2 > $len;
1712
+ my ($t, $n) = unpack("x${p}CC", $buff);
1713
+ last if $p + 2 + $n > $len;
1714
+ my $val = substr($buff, $p+2, $n);
1715
+ $et->HandleTag($tagTablePtr, $t, $val);
1716
+ $p += 2 + $n;
1717
+ }
1718
+ }
1719
+ ($pos -= 6) < $trailerStart and last; # step back to previous record
1720
+ $raf->Seek($pos, 0) or last;
1721
+ $raf->Read($buff, 6) == 6 or last;
1722
+ }
1723
+ SetByteOrder('MM');
1724
+ }
1725
+
1726
+ #------------------------------------------------------------------------------
1727
+ # Scan movie data for "freeGPS" metadata if not found already (ref PH)
1728
+ # Inputs: 0) ExifTool ref
1729
+ sub ScanMovieData($)
1730
+ {
1731
+ my $et = shift;
1732
+ my $raf = $$et{RAF} or return;
1733
+ my ($tagTbl, $oldByteOrder, $verbose, $buff, $dataLen);
1734
+ my ($pos, $buf2) = (0, '');
1735
+
1736
+ # don't rescan for freeGPS if we already found embedded metadata
1737
+ my $dataPos = $$et{VALUE}{MovieDataOffset};
1738
+ if ($dataPos and not $$et{DOC_COUNT}) {
1739
+ $dataLen = $$et{VALUE}{MovieDataSize};
1740
+ if ($dataLen) {
1741
+ if ($raf->Seek($dataPos, 0)) {
1742
+ $$et{FreeGPS2} = { }; # initialize variable space for FreeGPS2()
1743
+ } else {
1744
+ undef $dataLen;
1745
+ }
1746
+ }
1747
+ }
1748
+
1749
+ # loop through 'mdat' movie data looking for GPS information
1750
+ while ($dataLen) {
1751
+ last if $pos + $gpsBlockSize > $dataLen;
1752
+ last unless $raf->Read($buff, $gpsBlockSize);
1753
+ $buff = $buf2 . $buff if length $buf2;
1754
+ last if length $buff < $gpsBlockSize;
1755
+ # look for "freeGPS " block
1756
+ # (found on an absolute 0x8000-byte boundary in all of my samples,
1757
+ # but allow for any alignment when searching)
1758
+ if ($buff !~ /\0..\0freeGPS /sg) { # (seen ".." = "\0\x80","\x01\0")
1759
+ $buf2 = substr($buff,-12);
1760
+ $pos += length($buff)-12;
1761
+ # in all of my samples the first freeGPS block is within 2 MB of the start
1762
+ # of the mdat, so limit the scan to the first 20 MB to be fast and safe
1763
+ next if $tagTbl or $pos < 20e6;
1764
+ last;
1765
+ } elsif (not $tagTbl) {
1766
+ # initialize variables for extracting metadata from this block
1767
+ $tagTbl = GetTagTable('Image::ExifTool::QuickTime::Stream');
1768
+ $verbose = $$et{OPTIONS}{Verbose};
1769
+ $oldByteOrder = GetByteOrder();
1770
+ SetByteOrder('II');
1771
+ $et->VPrint(0, "---- Extract Embedded ----\n");
1772
+ $$et{INDENT} .= '| ';
1773
+ }
1774
+ if (pos($buff) > 12) {
1775
+ $pos += pos($buff) - 12;
1776
+ $buff = substr($buff, pos($buff) - 12);
1777
+ }
1778
+ # make sure we have the full freeGPS record
1779
+ my $len = unpack('N', $buff);
1780
+ if ($len < 12) {
1781
+ $len = 12;
1782
+ } else {
1783
+ my $more = $len - length($buff);
1784
+ if ($more > 0) {
1785
+ last unless $raf->Read($buf2, $more) == $more;
1786
+ $buff .= $buf2;
1787
+ }
1788
+ if ($verbose) {
1789
+ $et->VerboseDir('GPS', undef, $len);
1790
+ $et->VerboseDump(\$buff, DataPos => $pos + $dataPos);
1791
+ }
1792
+ my $dirInfo = { DataPt => \$buff, DataPos => $pos + $dataPos, DirLen => $len };
1793
+ ProcessFreeGPS2($et, $dirInfo, $tagTbl);
1794
+ }
1795
+ $pos += $len;
1796
+ $buf2 = substr($buff, $len);
1797
+ }
1798
+ if ($tagTbl) {
1799
+ $$et{DOC_NUM} = 0;
1800
+ $et->VPrint(0, "--------------------------\n");
1801
+ SetByteOrder($oldByteOrder);
1802
+ $$et{INDENT} = substr $$et{INDENT}, 0, -2;
1803
+ }
1804
+ # process INSV trailer if it exists
1805
+ ProcessINSVTrailer($et);
1806
+ }
1807
+
1808
+ 1; # end
1809
+
1810
+ __END__
1811
+
1812
+ =head1 NAME
1813
+
1814
+ Image::ExifTool::QuickTime - Extract embedded information from movie data
1815
+
1816
+ =head1 SYNOPSIS
1817
+
1818
+ These routines are autoloaded by Image::ExifTool::QuickTime.
1819
+
1820
+ =head1 DESCRIPTION
1821
+
1822
+ This file contains routines used by Image::ExifTool to extract embedded
1823
+ information like GPS tracks from MOV, MP4 and INSV movie data.
1824
+
1825
+ =head1 AUTHOR
1826
+
1827
+ Copyright 2003-2019, Phil Harvey (phil at owl.phy.queensu.ca)
1828
+
1829
+ This library is free software; you can redistribute it and/or modify it
1830
+ under the same terms as Perl itself.
1831
+
1832
+ =head1 REFERENCES
1833
+
1834
+ =over 4
1835
+
1836
+ =item Lhttps://developer.apple.com/library/content/documentation/QuickTime/QTFF/QTFFChap3/qtff3.html#//apple_ref/doc/uid/TP40000939-CH205-SW130>
1837
+
1838
+ =item L<http://sergei.nz/files/nvtk_mp42gpx.py>
1839
+
1840
+ =item L<https://forum.flitsservice.nl/dashcam-info/dod-ls460w-gps-data-uit-mov-bestand-lezen-t87926.html>
1841
+
1842
+ =item L<https://developers.google.com/streetview/publish/camm-spec>
1843
+
1844
+ =item L<https://sergei.nz/extracting-gps-data-from-viofo-a119-and-other-novatek-powered-cameras/>
1845
+
1846
+ =back
1847
+
1848
+ =head1 SEE ALSO
1849
+
1850
+ L<Image::ExifTool::QuickTime(3pm)|Image::ExifTool::QuickTime>,
1851
+ L<Image::ExifTool::TagNames/QuickTime Stream Tags>,
1852
+ L<Image::ExifTool::TagNames/GoPro GPMF Tags>,
1853
+ L<Image::ExifTool::TagNames/Sony rtmd Tags>,
1854
+ L<Image::ExifTool(3pm)|Image::ExifTool>
1855
+
1856
+ =cut
1857
+