summaryrefslogtreecommitdiff
path: root/lib/Image/ExifTool/QuickTimeStream.pl
diff options
context:
space:
mode:
authorexiftool <exiftool@users.sourceforge.net>2018-10-02 10:28:48 -0400
committerexiftool <exiftool@users.sourceforge.net>2018-10-02 10:28:48 -0400
commit5297201750bfe371010ee437f1d31aeec4ab49ab (patch)
treeaa27353ede1a7977ea2a97c1c07f9e2a5ea66861 /lib/Image/ExifTool/QuickTimeStream.pl
parent291f49c8f0ad6a36472f7e642853f926fa16ed0a (diff)
Update to 11.12
Diffstat (limited to 'lib/Image/ExifTool/QuickTimeStream.pl')
-rw-r--r--lib/Image/ExifTool/QuickTimeStream.pl566
1 files changed, 333 insertions, 233 deletions
diff --git a/lib/Image/ExifTool/QuickTimeStream.pl b/lib/Image/ExifTool/QuickTimeStream.pl
index 1cc33248..6a4eaa51 100644
--- a/lib/Image/ExifTool/QuickTimeStream.pl
+++ b/lib/Image/ExifTool/QuickTimeStream.pl
@@ -16,6 +16,8 @@ package Image::ExifTool::QuickTime;
use strict;
sub Process_tx3g($$$);
+sub ProcessFreeGPS($$$);
+sub ProcessFreeGPS2($$$);
# QuickTime data types that have ExifTool equivalents
# (ref https://developer.apple.com/library/content/documentation/QuickTime/QTFF/Metadata/Metadata.html#//apple_ref/doc/uid/TP40000939-CH1-SW35)
@@ -82,6 +84,10 @@ my %processByMetaFormat = (
GPSTrack => { PrintConv => 'sprintf("%.4f", $val) + 0' }, # round to 4 decimals
GPSTrackRef => { PrintConv => { M => 'Magnetic North', T => 'True North' } },
GPSDateTime => { PrintConv => '$self->ConvertDateTime($val)', Groups => { 2 => 'Time' } },
+ GPSTimeStamp => { PrintConv => 'Image::ExifTool::GPS::PrintTimeStamp($val)', Groups => { 2 => 'Time' } },
+ GPSSatellites=> { },
+ GPSDOP => { Description => 'GPS Dilution Of Precision' },
+ CameraDateTime=>{ PrintConv => '$self->ConvertDateTime($val)', Groups => { 2 => 'Time' } },
Accelerometer=> { Notes => 'right/up/backward acceleration in units of g' },
RawGSensor => {
# (same as GSensor, but offset by some unknown value)
@@ -734,56 +740,16 @@ sub ProcessSamples($)
$et->VPrint(0, "Unknown meta format ($metaFormat)");
}
- } elsif ($type eq 'gps ') {
-
- # decode Novatek GPS data
- next unless $buff =~ /^....freeGPS /s and length $buff >= 92;
- my ($hr,$min,$sec,$yr,$mon,$day,$stat,$latRef,$lonRef,$lat,$lon,$spd,$trk,$lbl,@acc);
- if (substr($buff,12,1) eq "\x05") {
- # ref 5: (models like DashCam Azdome GS63H)
- my $n = length($buff) - 18;
- $n = 0x101 if $n > 0x101;
- my $buf2 = pack 'C*', map { $_ ^ 0xaa } unpack 'C*', substr($buff,18,$n);
- if ($verbose > 1) {
- $et->VPrint(1, '[decrypted freeGPS data]');
- $et->VerboseDump(\$buf2);
- }
- # (extract longitude as 9 digits, not 8, ref PH)
- next 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;
- ($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);
- $spd += 0; # remove leading 0's
- $lbl =~ s/\0.*//s; $lbl =~ s/\s+$//; # truncate at null and remove trailing spaces
- # extract accelerometer data (ref PH)
- @acc = ($1/100,$2/100,$3/100) if $buf2 =~ /^.{173}([-+]\d{3})([-+]\d{3})([-+]\d{3})/s;
- } else {
- # ref 2: (see comments in ScanMovieData() below for structure details)
- ($hr,$min,$sec,$yr,$mon,$day,$stat,$latRef,$lonRef,$lat,$lon,$spd,$trk) =
- unpack('x48V6a1a1a1x1V4', $buff);
- # ignore invalid fixes
- next unless $stat eq 'A' and ($latRef eq 'N' or $latRef eq 'S') and
- ($lonRef eq 'E' or $lonRef eq 'W');
- ($lat,$lon,$spd,$trk) = unpack 'f*', pack 'L*', $lat, $lon, $spd, $trk;
- $yr += $yr >= 70 ? 1900 : 2000;
- $spd = $knotsToKph; # convert speed to km/h
- }
- FoundSomething($et, $tagTbl, $time[$i], $dur[$i]);
- # lat/long are in DDDmm.mmmm format
- my $deg = int($lat / 100);
- $lat = ($deg + ($lat - $deg * 100) / 60) * ($latRef eq 'S' ? -1 : 1);
- $deg = int($lon / 100);
- $lon = ($deg + ($lon - $deg * 100) / 60) * ($lonRef eq 'W' ? -1 : 1);
- my $time = sprintf('%.4d:%.2d:%.2d %.2d:%.2d:%.2dZ',$yr,$mon,$day,$hr,$min,$sec);
- $et->HandleTag($tagTbl, GPSDateTime => $time);
- $et->HandleTag($tagTbl, GPSLatitude => $lat);
- $et->HandleTag($tagTbl, GPSLongitude => $lon);
- $et->HandleTag($tagTbl, GPSSpeed => $spd);
- $et->HandleTag($tagTbl, GPSSpeedRef => 'K');
- if (defined $trk) {
- $et->HandleTag($tagTbl, GPSTrack => $trk); #PH (NC, could be GPSImageDirection)
- $et->HandleTag($tagTbl, GPSTrackRef => 'T');
+ } elsif ($type eq 'gps ') { # (ie. GPSDataList tag)
+
+ if ($buff =~ /^....freeGPS /s) {
+ # decode "freeGPS " data (Novatek)
+ ProcessFreeGPS($et, {
+ DataPt => \$buff,
+ SampleTime => $time[$i],
+ SampleDuration => $dur[$i],
+ }, $tagTbl) ;
}
- $et->HandleTag($tagTbl, Accelerometer => \@acc) if @acc;
- $et->HandleTag($tagTbl, UserLabel => $lbl) if defined $lbl and length $lbl;
} elsif ($$tagTbl{$type}) {
@@ -809,6 +775,308 @@ sub ProcessSamples($)
}
#------------------------------------------------------------------------------
+# Process "freeGPS " data blocks referenced by a 'gps ' (GPSDataList) atom
+# Inputs: 0) ExifTool ref, 1) dirInfo ref (with SampleTime and SampleDuration), 2) tagTable ref
+# Returns: 1 on success (or 0 on unrecognized or "measurement-void" GPS data)
+# Notes:
+# - also see ProcessFreeGPS2() below for processing of other types of freeGPS blocks
+sub ProcessFreeGPS($$$)
+{
+ my ($et, $dirInfo, $tagTbl) = @_;
+ my $dataPt = $$dirInfo{DataPt};
+ my ($yr, $mon, $day, $hr, $min, $sec, $stat, $lbl);
+ my ($lat, $latRef, $lon, $lonRef, $spd, $trk, $alt, @acc, @xtra);
+
+ return 0 unless length $$dataPt >= 92;
+
+ if (substr($$dataPt,12,1) eq "\x05") {
+ # decode encrypted ASCII-based GPS (DashCam Azdome GS63H, ref 5)
+ my $n = length($$dataPt) - 18;
+ $n = 0x101 if $n > 0x101;
+ my $buf2 = pack 'C*', map { $_ ^ 0xaa } unpack 'C*', substr($$dataPt,18,$n);
+ if ($et->Options('Verbose') > 1) {
+ $et->VPrint(1, '[decrypted freeGPS data]');
+ $et->VerboseDump(\$buf2);
+ }
+ # (extract longitude as 9 digits, not 8, ref PH)
+ 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;
+ ($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);
+ $spd += 0; # remove leading 0's
+ $lbl =~ s/\0.*//s; $lbl =~ s/\s+$//; # truncate at null and remove trailing spaces
+ push @xtra, UserLabel => $lbl if length $lbl;
+ # extract accelerometer data (ref PH)
+ @acc = ($1/100,$2/100,$3/100) if $buf2 =~ /^.{173}([-+]\d{3})([-+]\d{3})([-+]\d{3})/s;
+ } elsif ($$dataPt =~ /^.{52}(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})/) {
+ # decode NMEA-format GPS data (NextBase 512GW dashcam, ref PH)
+ push @xtra, CameraDateTime => "$1:$2:$3 $4:$5:$6";
+ 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) {
+ ($lat,$latRef,$lon,$lonRef) = ($5,$6,$7,$8);
+ $yr = $13 + ($13 >= 70 ? 1900 : 2000);
+ ($mon,$day,$hr,$min,$sec) = ($12,$11,$1,$2,$3);
+ $spd = $9 * $knotsToKph if length $9;
+ $trk = $10 if length $10;
+ }
+ 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) {
+ ($hr,$min,$sec,$lat,$latRef,$lon,$lonRef) = ($1,$2,$3,$5,$6,$7,$8) unless defined $yr;
+ $alt = $11;
+ unshift @xtra, GPSSatellites => $9;
+ unshift @xtra, GPSDOP => $10;
+ }
+ if (defined $lat) {
+ # extract accelerometer readings if GPS was valid
+ @acc = unpack('x68V3', $$dataPt);
+ # change to signed integer and divide by 256
+ map { $_ = $_ - 4294967296 if $_ >= 2147483648; $_ /= 256 } @acc;
+ }
+ } else {
+ # decode binary GPS format (Viofo A119S, ref 2)
+ # (see comments in ScanMovieData() below for structure details)
+ ($hr,$min,$sec,$yr,$mon,$day,$stat,$latRef,$lonRef,$lat,$lon,$spd,$trk) =
+ unpack('x48V6a1a1a1x1V4', $$dataPt);
+ # ignore invalid fixes
+ return 0 unless $stat eq 'A' and ($latRef eq 'N' or $latRef eq 'S') and
+ ($lonRef eq 'E' or $lonRef eq 'W');
+ ($lat,$lon,$spd,$trk) = unpack 'f*', pack 'L*', $lat, $lon, $spd, $trk;
+ $yr += $yr >= 70 ? 1900 : 2000;
+ $spd *= $knotsToKph; # convert speed to km/h
+ # ($trk is not confirmed; may be GPSImageDirection, ref PH)
+ }
+ FoundSomething($et, $tagTbl, $$dirInfo{SampleTime}, $$dirInfo{SampleDuration});
+ # lat/long are in DDDmm.mmmm format
+ my $deg = int($lat / 100);
+ $lat = $deg + ($lat - $deg * 100) / 60;
+ $deg = int($lon / 100);
+ $lon = $deg + ($lon - $deg * 100) / 60;
+ $sec = '0' . $sec unless $sec =~ /^\d{2}/; # pad integer part of seconds to 2 digits
+ if (defined $yr) {
+ my $time = sprintf('%.4d:%.2d:%.2d %.2d:%.2d:%sZ',$yr,$mon,$day,$hr,$min,$sec);
+ $et->HandleTag($tagTbl, GPSDateTime => $time);
+ } elsif (defined $hr) {
+ my $time = sprintf('%.2d:%.2d:%.2dZ',$hr,$min,$sec);
+ $et->HandleTag($tagTbl, GPSTimeStamp => $time);
+ }
+ $et->HandleTag($tagTbl, GPSLatitude => $lat * ($latRef eq 'S' ? -1 : 1));
+ $et->HandleTag($tagTbl, GPSLongitude => $lon * ($lonRef eq 'W' ? -1 : 1));
+ $et->HandleTag($tagTbl, GPSAltitude => $alt) if defined $alt;
+ if (defined $spd) {
+ $et->HandleTag($tagTbl, GPSSpeed => $spd);
+ $et->HandleTag($tagTbl, GPSSpeedRef => 'K');
+ }
+ if (defined $trk) {
+ $et->HandleTag($tagTbl, GPSTrack => $trk);
+ $et->HandleTag($tagTbl, GPSTrackRef => 'T');
+ }
+ while (@xtra) {
+ my $tag = shift @xtra;
+ $et->HandleTag($tagTbl, $tag => shift @xtra);
+ }
+ $et->HandleTag($tagTbl, Accelerometer => \@acc) if @acc;
+ return 1;
+}
+
+#------------------------------------------------------------------------------
+# Process "freeGPS " data blocks _not_ referenced by a 'gps ' atom
+# Inputs: 0) ExifTool ref, 1) dirInfo ref, 2) tagTable ref
+# Returns: 1 on success
+# Notes:
+# - also see ProcessFreeGPS() above
+# - on entry, the length of $$dataPt will be at least $gpsBlockSize bytes long
+sub ProcessFreeGPS2($$$)
+{
+ my ($et, $dirInfo, $tagTbl) = @_;
+ my $dataPt = $$dirInfo{DataPt};
+
+ if (substr($$dataPt,0x45,3) eq 'ATC') {
+
+ my ($recPos, $lastRecPos, $foundNew);
+ my $verbose = $et->Options('Verbose');
+ my $dataPos = $$dirInfo{DataPos};
+ my $then = $$et{FreeGPS2}{Then};
+ $then or $then = $$et{FreeGPS2}{Then} = [ (0) x 6 ];
+
+ # Loop through records in the ATC-type GPS block until we find the most recent.
+ # If we have already found one, then we only need to check the first record
+ # (in case the buffer wrapped around), and the record after the position of
+ # the last record we found, because the others will be old. Odd, but this
+ # is the way it is done... I have only seen one new 52-byte record in the
+ # entire 32 kB block, but the entire device ring buffer (containing 30
+ # entries in my samples) is stored every time. The code below allows for
+ # the possibility of missing blocks and multiple new records in a single
+ # block, but I have never seen this. Note that there may be some earlier
+ # GPS records at the end of the first block that we will miss decoding, but
+ # these should (I believe) be before the start of the video
+ATCRec: for ($recPos = 0x30; $recPos + 52 < $gpsBlockSize; $recPos += 52) {
+
+ my $a = substr($$dataPt, $recPos, 52); # isolate a single record
+ # decrypt record
+ my @a = unpack('C*', $a);
+ my ($key1, $key2) = @a[0x14, 0x1c];
+ $a[$_] ^= $key1 foreach 0x00..0x14, 0x18..0x1b;
+ $a[$_] ^= $key2 foreach 0x1c, 0x20..0x32;
+ my $b = pack 'C*', @a;
+ # unpack and validate date/time
+ my @now = unpack 'x13C3x28vC2', $b; # (H-1,M,S,Y,m,d)
+ $now[0] = ($now[0] + 1) & 0xff; # increment hour
+ my $i;
+ for ($i=0; $i<@dateMax; ++$i) {
+ next if $now[$i] <= $dateMax[$i];
+ $et->WarnOnce('Invalid GPS date/time');
+ next ATCRec; # ignore this record
+ }
+ # look for next ATC record in temporal sequence
+ foreach $i (3..5, 0..2) {
+ if ($now[$i] < $$then[$i]) {
+ last ATCRec if $foundNew;
+ last;
+ }
+ next if $now[$i] == $$then[$i];
+ # we found a more recent record -- extract it and remember its location
+ if ($verbose) {
+ $et->VPrint(2, " Encrypted GPS record:\n");
+ $et->VerboseDump(\$a, DataPos => $dataPos + $recPos);
+ $et->VPrint(0, " Decrypted GPS record:\n");
+ $et->VerboseDump(\$b);
+ my @v = unpack 'H8VVC4V!CA3V!CA3VvvV!vCCCCH4', $b;
+ $et->VPrint(1, " Unpacked: @v\n");
+ # values unpacked above (ref PH):
+ # 0) 0x00 4 bytes - byte 0=1, 1=counts to 255, 2=record index, 3=0 (ref 3)
+ # 1) 0x04 4 bytes - int32u: bits 0-4=day, 5-8=mon, 9-19=year (ref 3)
+ # 2) 0x08 4 bytes - int32u: bits 0-5=sec, 6-11=min, 12-16=hour (ref 3)
+ # 3) 0x0c 1 byte - seen values of 0,1,2 - GPS status maybe?
+ # 4) 0x0d 1 byte - hour minus 1
+ # 5) 0x0e 1 byte - minute
+ # 6) 0x0f 1 byte - second
+ # 7) 0x10 4 bytes - int32s latitude * 1e7
+ # 8) 0x14 1 byte - always 0 (used for decryption)
+ # 9) 0x15 3 bytes - always "ATC"
+ # 10) 0x18 4 bytes - int32s longitude * 1e7
+ # 11) 0x1c 1 byte - always 0 (used for decryption)
+ # 12) 0x1d 3 bytes - always "001"
+ # 13) 0x20 4 bytes - int32s speed * 100 (m/s)
+ # 14) 0x24 2 bytes - int16u heading * 100 (-180 to 180 deg)
+ # 15) 0x26 2 bytes - always zero
+ # 16) 0x28 4 bytes - int32s altitude * 1000 (ref 3)
+ # 17) 0x2c 2 bytes - int16u year
+ # 18) 0x2e 1 byte - month
+ # 19) 0x2f 1 byte - day
+ # 20) 0x30 1 byte - unknown
+ # 21) 0x31 1 byte - always zero
+ # 22) 0x32 2 bytes - checksum ?
+ }
+ @$then = @now;
+ $$et{DOC_NUM} = ++$$et{DOC_COUNT};
+ my $trk = Get16s(\$b, 0x24) / 100;
+ $trk += 360 if $trk < 0;
+ my $time = sprintf('%.4d:%.2d:%.2d %.2d:%.2d:%.2dZ', @now[3..5, 0..2]);
+ $et->HandleTag($tagTbl, GPSDateTime => $time);
+ $et->HandleTag($tagTbl, GPSLatitude => Get32s(\$b, 0x10) / 1e7);
+ $et->HandleTag($tagTbl, GPSLongitude => Get32s(\$b, 0x18) / 1e7);
+ $et->HandleTag($tagTbl, GPSSpeed => Get32s(\$b, 0x20) / 100 * $mpsToKph);
+ $et->HandleTag($tagTbl, GPSSpeedRef => 'K');
+ $et->HandleTag($tagTbl, GPSTrack => $trk);
+ $et->HandleTag($tagTbl, GPSTrackRef => 'T');
+ $et->HandleTag($tagTbl, GPSAltitude => Get32s(\$b, 0x28) / 1000);
+ $lastRecPos = $recPos;
+ $foundNew = 1;
+ # don't skip to location of previous recent record in ring buffer
+ # since we found a more recent record here
+ delete $$et{FreeGPS2}{RecentRecPos};
+ last;
+ }
+ # skip older records
+ my $recentRecPos = $$et{FreeGPS2}{RecentRecPos};
+ $recPos = $recentRecPos if $recentRecPos and $recPos < $recentRecPos;
+ }
+ # save position of most recent record (needed when parsing the next freeGPS block)
+ $$et{FreeGPS2}{RecentRecPos} = $lastRecPos;
+
+ } else {
+
+ my ($yr, $mon, $day, $hr, $min, $sec);
+ my ($lat, $latRef, $lon, $lonRef, $spd, $trk, $alt, $ddd);
+ if ($$dataPt =~ /^.{60}A\0.{10}([NS])\0.{14}([EW])\0/s) {
+ # Type 2 (ref PH):
+ # 0x30 - int32u hour
+ # 0x34 - int32u minute
+ # 0x38 - int32u second
+ # 0x3c - int32u GPS status ('A' or 'V')
+ # 0x40 - double latitude (DDMM.MMMMMM)
+ # 0x48 - int32u latitude ref ('N' or 'S')
+ # 0x50 - double longitude (DDMM.MMMMMM)
+ # 0x58 - int32u longitude ref ('E' or 'W')
+ # 0x60 - double speed (knots)
+ # 0x68 - double heading (deg)
+ # 0x70 - int32u year - 2000
+ # 0x74 - int32u month
+ # 0x78 - int32u day
+ ($latRef, $lonRef) = ($1, $2);
+ ($hr,$min,$sec,$yr,$mon,$day) = unpack('x' . 0x30 . 'V3x52V3', $$dataPt);
+ $lat = GetDouble($dataPt, 0x40);
+ $lon = GetDouble($dataPt, 0x50);
+ $spd = GetDouble($dataPt, 0x60) * $knotsToKph;
+ $trk = GetDouble($dataPt, 0x68);
+ } elsif ($$dataPt =~ /^.{72}A([NS])([EW])/s) {
+ # Type 3 (Novatek GPS, ref 2): (in case it wasn't decoded via 'gps ' atom)
+ # 0x30 - int32u hour
+ # 0x34 - int32u minute
+ # 0x38 - int32u second
+ # 0x3c - int32u year - 2000
+ # 0x40 - int32u month
+ # 0x44 - int32u day
+ # 0x48 - int8u GPS status ('A' or 'V')
+ # 0x49 - int8u latitude ref ('N' or 'S')
+ # 0x4a - int8u longitude ref ('E' or 'W')
+ # 0x4b - 0
+ # 0x4c - float latitude (DDMM.MMMMMM)
+ # 0x50 - float longitude (DDMM.MMMMMM)
+ # 0x54 - float speed (knots)
+ # 0x58 - float heading (deg)
+ ($latRef, $lonRef) = ($1, $2);
+ ($hr,$min,$sec,$yr,$mon,$day) = unpack('x' . 0x30 . 'V6', $$dataPt);
+ if (substr($$dataPt, 16, 3) eq 'IQS') {
+ $ddd = 1;
+ $lat = abs Get32s($dataPt, 0x4c) / 1e7;
+ $lon = abs Get32s($dataPt, 0x50) / 1e7;
+ $spd = Get32s($dataPt, 0x54) / 100 * $mpsToKph;
+ $alt = GetFloat($dataPt, 0x58) / 1000; # (NC)
+ } else {
+ $lat = GetFloat($dataPt, 0x4c);
+ $lon = GetFloat($dataPt, 0x50);
+ $spd = GetFloat($dataPt, 0x54) * $knotsToKph;
+ $trk = GetFloat($dataPt, 0x58);
+ }
+ } else {
+ return 0; # not a recognized or valid freeGPS block
+ }
+ return 0 if $mon < 1 or $mon > 12; # quick sanity check
+ $$et{DOC_NUM} = ++$$et{DOC_COUNT};
+ $yr += 2000 if $yr < 2000;
+ my $time = sprintf('%.4d:%.2d:%.2d %.2d:%.2d:%.2dZ', $yr, $mon, $day, $hr, $min, $sec);
+ # convert from DDMM.MMMMMM to DD.DDDDDD format if necessary
+ unless ($ddd) {
+ my $deg = int($lat / 100);
+ $lat = $deg + ($lat - $deg * 100) / 60;
+ $deg = int($lon / 100);
+ $lon = $deg + ($lon - $deg * 100) / 60;
+ }
+ $et->HandleTag($tagTbl, GPSDateTime => $time);
+ $et->HandleTag($tagTbl, GPSLatitude => $lat * ($latRef eq 'S' ? -1 : 1));
+ $et->HandleTag($tagTbl, GPSLongitude => $lon * ($lonRef eq 'W' ? -1 : 1));
+ $et->HandleTag($tagTbl, GPSSpeed => $spd); # (now in km/h)
+ $et->HandleTag($tagTbl, GPSSpeedRef => 'K');
+ if (defined $trk) {
+ $et->HandleTag($tagTbl, GPSTrack => $trk);
+ $et->HandleTag($tagTbl, GPSTrackRef => 'T');
+ }
+ if (defined $alt) {
+ $et->HandleTag($tagTbl, GPSAltitude => $alt);
+ }
+ }
+
+ return 1;
+}
+
+#------------------------------------------------------------------------------
# Extract embedded information referenced from a track
# Inputs: 0) ExifTool ref, 1) tag name, 2) data ref
sub ParseTag($$$)
@@ -951,10 +1219,11 @@ sub ScanMovieData($)
my $dataPos = $$et{VALUE}{MovieDataOffset} or return;
my $dataLen = $$et{VALUE}{MovieDataSize} or return;
$raf->Seek($dataPos, 0) or return;
- my @then = (0) x 6;
- my ($pos, $recentRecPos, $buf2) = (0, 0, '');
+ my ($pos, $buf2) = (0, '');
my ($tagTbl, $oldByteOrder, $verbose, $buff);
+ $$et{FreeGPS2} = { }; # initialize variable space for FreeGPS2()
+
# loop through 'mdat' movie data looking for GPS information
for (;;) {
last if $pos + $gpsBlockSize > $dataLen;
@@ -980,197 +1249,24 @@ sub ScanMovieData($)
$et->VPrint(0, "---- Extract Embedded ----\n");
$$et{INDENT} .= '| ';
}
- my $gpsPos = pos($buff) - 12;
+ if (pos($buff) > 12) {
+ $pos += pos($buff) - 12;
+ $buff = substr($buff, pos($buff) - 12);
+ }
# make sure we have the full 0x8000-byte freeGPS record
- my $more = $gpsBlockSize - (length($buff) - $gpsPos);
+ my $more = $gpsBlockSize - length($buff);
if ($more > 0) {
last unless $raf->Read($buf2, $more) == $more;
$buff .= $buf2;
}
if ($verbose) {
$et->VerboseDir('GPS', undef, $gpsBlockSize);
- $et->VerboseDump(\$buff, Start => $gpsPos, DataPos => $pos + $dataPos);
+ $et->VerboseDump(\$buff, DataPos => $pos + $dataPos);
}
- # Loop through records in the ATC-type GPS block until we find the most recent.
- # If we have alread found one, then we only need to check the first record
- # (in case the buffer wrapped around), and the record after the position of
- # the last record we found, because the others will be old. Odd, but this
- # is the way it is done... I have only seen one new 52-byte record in the
- # entire 32 kB block, but the entire device ring buffer (containing 30
- # entries in my samples) is stored every time. The code below allows for
- # the possibility of missing blocks and multiple new records in a single
- # block, but I have never seen this. Note that there may be some earlier
- # GPS records at the end of the first block that we will miss decoding, but
- # these should (I believe) be before the start of the video
- my ($recPos, $lastRecPos, $foundNew);
-ATCRec: for ($recPos = 0x30; $recPos + 52 < $gpsBlockSize; $recPos += 52) {
+ ProcessFreeGPS2($et, { DataPt => \$buff, DataPos => $pos + $dataPos }, $tagTbl);
- # identify one type of GPS record using "ATC" string
- unless (substr($buff, $recPos + $gpsPos + 0x15, 3) eq 'ATC') {
- my ($yr, $mon, $day, $hr, $min, $sec);
- my ($lat, $latRef, $lon, $lonRef, $spd, $trk, $alt, $ddd);
- # check for other known GPS record types
- # Type 2 (ref PH):
- # 0x30 - int32u hour
- # 0x34 - int32u minute
- # 0x38 - int32u second
- # 0x3c - int32u GPS status ('A' or 'V')
- # 0x40 - double latitude (DDMM.MMMMMM)
- # 0x48 - int32u latitude ref ('N' or 'S')
- # 0x50 - double longitude (DDMM.MMMMMM)
- # 0x58 - int32u longitude ref ('E' or 'W')
- # 0x60 - double speed (knots)
- # 0x68 - double heading (deg)
- # 0x70 - int32u year - 2000
- # 0x74 - int32u month
- # 0x78 - int32u day
- if ($buff =~ /^.{$gpsPos}.{60}A\0.{10}([NS])\0.{14}([EW])\0/s) {
- ($latRef, $lonRef) = ($1, $2);
- ($hr,$min,$sec,$yr,$mon,$day) = unpack('x'.($gpsPos+0x30).'V3x52V3', $buff);
- $lat = GetDouble(\$buff, $gpsPos + 0x40);
- $lon = GetDouble(\$buff, $gpsPos + 0x50);
- $spd = GetDouble(\$buff, $gpsPos + 0x60) * $knotsToKph;
- $trk = GetDouble(\$buff, $gpsPos + 0x68);
- # Type 3 (Novatek GPS, ref 2): (in case it wasn't decoded via 'gps ' atom)
- # 0x30 - int32u hour
- # 0x34 - int32u minute
- # 0x38 - int32u second
- # 0x3c - int32u year - 2000
- # 0x40 - int32u month
- # 0x44 - int32u day
- # 0x48 - int8u GPS status ('A' or 'V')
- # 0x49 - int8u latitude ref ('N' or 'S')
- # 0x4a - int8u longitude ref ('E' or 'W')
- # 0x4b - 0
- # 0x4c - float latitude (DDMM.MMMMMM)
- # 0x50 - float longitude (DDMM.MMMMMM)
- # 0x54 - float speed (knots)
- # 0x58 - float heading (deg)
- } elsif ($buff =~ /^.{$gpsPos}.{72}A([NS])([EW])/s) {
- ($latRef, $lonRef) = ($1, $2);
- ($hr,$min,$sec,$yr,$mon,$day) = unpack('x'.($gpsPos+0x30).'V6', $buff);
- if (substr($buff,$gpsPos+16, 3) eq 'IQS') {
- $ddd = 1;
- $lat = abs Get32s(\$buff, $gpsPos + 0x4c) / 1e7;
- $lon = abs Get32s(\$buff, $gpsPos + 0x50) / 1e7;
- $spd = Get32s(\$buff, $gpsPos + 0x54) / 100 * $mpsToKph;
- $alt = GetFloat(\$buff, $gpsPos + 0x58) / 1000; # (NC)
- } else {
- $lat = GetFloat(\$buff, $gpsPos + 0x4c);
- $lon = GetFloat(\$buff, $gpsPos + 0x50);
- $spd = GetFloat(\$buff, $gpsPos + 0x54) * $knotsToKph;
- $trk = GetFloat(\$buff, $gpsPos + 0x58);
- }
- } else {
- last; # not a recognized or valid freeGPS block
- }
- last if $mon < 1 or $mon > 12; # quick sanity check
- $$et{DOC_NUM} = ++$$et{DOC_COUNT};
- $yr += 2000 if $yr < 2000;
- my $time = sprintf('%.4d:%.2d:%.2d %.2d:%.2d:%.2dZ', $yr, $mon, $day, $hr, $min, $sec);
- # convert from DDMM.MMMMMM to DD.DDDDDD format if necessary
- unless ($ddd) {
- my $deg = int($lat / 100);
- $lat = $deg + ($lat - $deg * 100) / 60;
- $deg = int($lon / 100);
- $lon = $deg + ($lon - $deg * 100) / 60;
- }
- $et->HandleTag($tagTbl, GPSDateTime => $time);
- $et->HandleTag($tagTbl, GPSLatitude => $lat * ($latRef eq 'S' ? -1 : 1));
- $et->HandleTag($tagTbl, GPSLongitude => $lon * ($lonRef eq 'W' ? -1 : 1));
- $et->HandleTag($tagTbl, GPSSpeed => $spd); # (now in km/h)
- $et->HandleTag($tagTbl, GPSSpeedRef => 'K');
- if (defined $trk) {
- $et->HandleTag($tagTbl, GPSTrack => $trk);
- $et->HandleTag($tagTbl, GPSTrackRef => 'T');
- }
- if (defined $alt) {
- $et->HandleTag($tagTbl, GPSAltitude => $alt);
- }
- last;
- }
- my $a = substr($buff, $recPos + $gpsPos, 52); # isolate a single record
- # decrypt record
- my @a = unpack('C*', $a);
- my ($key1, $key2) = @a[0x14, 0x1c];
- $a[$_] ^= $key1 foreach 0x00..0x14, 0x18..0x1b;
- $a[$_] ^= $key2 foreach 0x1c, 0x20..0x32;
- my $b = pack 'C*', @a;
- # unpack and validate date/time
- my @now = unpack 'x13C3x28vC2', $b; # (H-1,M,S,Y,m,d)
- $now[0] = ($now[0] + 1) & 0xff; # increment hour
- my $i;
- for ($i=0; $i<@dateMax; ++$i) {
- next if $now[$i] <= $dateMax[$i];
- $et->WarnOnce('Invalid GPS date/time');
- next ATCRec; # ignore this record
- }
- # look for next ATC record in temporal sequence
- foreach $i (3..5, 0..2) {
- if ($now[$i] < $then[$i]) {
- last ATCRec if $foundNew;
- last;
- }
- next if $now[$i] == $then[$i];
- # we found a more recent record -- extract it and remember its location
- if ($verbose) {
- $et->VPrint(2, " Encrypted GPS record:\n");
- $et->VerboseDump(\$a, DataPos => $pos + $dataPos + $gpsPos + $recPos);
- $et->VPrint(0, " Decrypted GPS record:\n");
- $et->VerboseDump(\$b);
- my @v = unpack 'H8VVC4V!CA3V!CA3VvvV!vCCCCH4', $b;
- $et->VPrint(1, " Unpacked: @v\n");
- # values unpacked above (ref PH):
- # 0) 0x00 4 bytes - byte 0=1, 1=counts to 255, 2=record index, 3=0 (ref 3)
- # 1) 0x04 4 bytes - int32u: bits 0-4=day, 5-8=mon, 9-19=year (ref 3)
- # 2) 0x08 4 bytes - int32u: bits 0-5=sec, 6-11=min, 12-16=hour (ref 3)
- # 3) 0x0c 1 byte - seen values of 0,1,2 - GPS status maybe?
- # 4) 0x0d 1 byte - hour minus 1
- # 5) 0x0e 1 byte - minute
- # 6) 0x0f 1 byte - second
- # 7) 0x10 4 bytes - int32s latitude * 1e7
- # 8) 0x14 1 byte - always 0 (used for decryption)
- # 9) 0x15 3 bytes - always "ATC"
- # 10) 0x18 4 bytes - int32s longitude * 1e7
- # 11) 0x1c 1 byte - always 0 (used for decryption)
- # 12) 0x1d 3 bytes - always "001"
- # 13) 0x20 4 bytes - int32s speed * 100 (m/s)
- # 14) 0x24 2 bytes - int16u heading * 100 (-180 to 180 deg)
- # 15) 0x26 2 bytes - always zero
- # 16) 0x28 4 bytes - int32s altitude * 1000 (ref 3)
- # 17) 0x2c 2 bytes - int16u year
- # 18) 0x2e 1 byte - month
- # 19) 0x2f 1 byte - day
- # 20) 0x30 1 byte - unknown
- # 21) 0x31 1 byte - always zero
- # 22) 0x32 2 bytes - checksum ?
- }
- @then = @now;
- $$et{DOC_NUM} = ++$$et{DOC_COUNT};
- my $trk = Get16s(\$b, 0x24) / 100;
- $trk += 360 if $trk < 0;
- my $time = sprintf('%.4d:%.2d:%.2d %.2d:%.2d:%.2dZ', @now[3..5, 0..2]);
- $et->HandleTag($tagTbl, GPSDateTime => $time);
- $et->HandleTag($tagTbl, GPSLatitude => Get32s(\$b, 0x10) / 1e7);
- $et->HandleTag($tagTbl, GPSLongitude => Get32s(\$b, 0x18) / 1e7);
- $et->HandleTag($tagTbl, GPSSpeed => Get32s(\$b, 0x20) / 100 * $mpsToKph);
- $et->HandleTag($tagTbl, GPSSpeedRef => 'K');
- $et->HandleTag($tagTbl, GPSTrack => $trk);
- $et->HandleTag($tagTbl, GPSTrackRef => 'T');
- $et->HandleTag($tagTbl, GPSAltitude => Get32s(\$b, 0x28) / 1000);
- $lastRecPos = $recPos;
- $foundNew = 1;
- # don't skip to location of previous recent record in ring buffer
- # since we found a more recent record here
- undef $recentRecPos;
- last;
- }
- # skip older records
- $recPos = $recentRecPos if $recentRecPos and $recPos < $recentRecPos;
- }
- $recentRecPos = $lastRecPos;
- $pos += $gpsPos + $gpsBlockSize;
- $buf2 = substr($buff, $gpsPos + $gpsBlockSize);
+ $pos += $gpsBlockSize;
+ $buf2 = substr($buff, $gpsBlockSize);
}
if ($tagTbl) {
$$et{DOC_NUM} = 0;
@@ -1214,6 +1310,10 @@ under the same terms as Perl itself.
=item L<https://forum.flitsservice.nl/dashcam-info/dod-ls460w-gps-data-uit-mov-bestand-lezen-t87926.html>
+=item L<https://developers.google.com/streetview/publish/camm-spec>
+
+=item L<https://sergei.nz/extracting-gps-data-from-viofo-a119-and-other-novatek-powered-cameras/>
+
=back
=head1 SEE ALSO