: str_replace(): Passing null to parameter #2 ($replace) of type array|string is deprecated in
while (($mdat_offset < (strlen($atom_data) - 8))
&& ($chapter_string_length = getid3_lib::BigEndian2Int(substr($atom_data, $mdat_offset, 2)))
&& ($chapter_string_length < 1000)
&& ($chapter_string_length <= (strlen($atom_data) - $mdat_offset - 2))
&& preg_match('#^([\x00-\xFF]{2})([\x20-\xFF]+)$#', substr($atom_data, $mdat_offset, $chapter_string_length + 2), $chapter_matches)) {
list($dummy, $chapter_string_length_hex, $chapter_string) = $chapter_matches;
$mdat_offset += (2 + $chapter_string_length);
@$info['quicktime']['comments']['chapters'][] = $chapter_string;
// "encd" atom specifies encoding. In theory could be anything, almost always UTF-8, but may be UTF-16 with BOM (not currently handled)
if (substr($atom_data, $mdat_offset, 12) == "\x00\x00\x00\x0C\x65\x6E\x63\x64\x00\x00\x01\x00") { // UTF-8
if (($atomsize > 8) && (!isset($info['avdataend_tmp']) || ($info['quicktime'][$atomname]['size'] > ($info['avdataend_tmp'] - $info['avdataoffset'])))) {
$info['avdataoffset'] = $atom_structure['offset'] + 8; // $info['quicktime'][$atomname]['offset'] + 8;
$OldAVDataEnd = $info['avdataend'];
$info['avdataend'] = $atom_structure['offset'] + $atom_structure['size']; // $info['quicktime'][$atomname]['offset'] + $info['quicktime'][$atomname]['size'];
$getid3_temp = new getID3();
$getid3_temp->openfile($this->getid3->filename, $this->getid3->info['filesize'], $this->getid3->fp);
$getid3_temp->info['avdataoffset'] = $info['avdataoffset'];
$getid3_temp->info['avdataend'] = $info['avdataend'];
$getid3_mp3 = new getid3_mp3($getid3_temp);
if ($getid3_mp3->MPEGaudioHeaderValid($getid3_mp3->MPEGaudioHeaderDecode($this->fread(4)))) {
$getid3_mp3->getOnlyMPEGaudioInfo($getid3_temp->info['avdataoffset'], false);
if (!empty($getid3_temp->info['warning'])) {
foreach ($getid3_temp->info['warning'] as $value) {
if (!empty($getid3_temp->info['mpeg'])) {
$info['mpeg'] = $getid3_temp->info['mpeg'];
if (isset($info['mpeg']['audio'])) {
$info['audio']['dataformat'] = 'mp3';
$info['audio']['codec'] = (!empty($info['mpeg']['audio']['encoder']) ? $info['mpeg']['audio']['encoder'] : (!empty($info['mpeg']['audio']['codec']) ? $info['mpeg']['audio']['codec'] : (!empty($info['mpeg']['audio']['LAME']) ? 'LAME' :'mp3')));
$info['audio']['sample_rate'] = $info['mpeg']['audio']['sample_rate'];
$info['audio']['channels'] = $info['mpeg']['audio']['channels'];
$info['audio']['bitrate'] = $info['mpeg']['audio']['bitrate'];
$info['audio']['bitrate_mode'] = strtolower($info['mpeg']['audio']['bitrate_mode']);
$info['bitrate'] = $info['audio']['bitrate'];
unset($getid3_mp3, $getid3_temp);
$info['avdataend'] = $OldAVDataEnd;
unset($mdat_offset, $chapter_string_length, $chapter_matches);
getid3_lib::IncludeDependency(GETID3_INCLUDEPATH.'module.tag.id3v2.php', __FILE__, true);
$getid3_temp = new getID3();
$getid3_temp->openfile($this->getid3->filename, $this->getid3->info['filesize'], $this->getid3->fp);
$getid3_id3v2 = new getid3_id3v2($getid3_temp);
$getid3_id3v2->StartingOffset = $atom_structure['offset'] + 14; // framelength(4)+framename(4)+flags(4)+??(2)
if ($atom_structure['valid'] = $getid3_id3v2->Analyze()) {
$atom_structure['id3v2'] = $getid3_temp->info['id3v2'];
$this->warning('ID32 frame at offset '.$atom_structure['offset'].' did not parse');
unset($getid3_temp, $getid3_id3v2);
case 'free': // FREE space atom
case 'skip': // SKIP atom
case 'wide': // 64-bit expansion placeholder atom
// 'free', 'skip' and 'wide' are just padding, contains no useful data at all
// When writing QuickTime files, it is sometimes necessary to update an atom's size.
// It is impossible to update a 32-bit atom to a 64-bit atom since the 32-bit atom
// is only 8 bytes in size, and the 64-bit atom requires 16 bytes. Therefore, QuickTime
// puts an 8-byte placeholder atom before any atoms it may have to update the size of.
// In this way, if the atom needs to be converted from a 32-bit to a 64-bit atom, the
// placeholder atom can be overwritten to obtain the necessary 8 extra bytes.
// The placeholder atom has a type of kWideAtomPlaceholderType ( 'wide' ).
case 'nsav': // NoSAVe atom
// http://developer.apple.com/technotes/tn/tn2038.html
$atom_structure['data'] = getid3_lib::BigEndian2Int(substr($atom_data, 0, 4));
case 'ctyp': // Controller TYPe atom (seen on QTVR)
// http://homepages.slingshot.co.nz/~helmboy/quicktime/formats/qtm-layout.txt
// some controller names are:
// 0x00 + 'std' for linear movie
// 'none' for no controls
$atom_structure['ctyp'] = substr($atom_data, 0, 4);
$info['quicktime']['controller'] = $atom_structure['ctyp'];
switch ($atom_structure['ctyp']) {
$info['video']['dataformat'] = 'quicktimevr';
case 'pano': // PANOrama track (seen on QTVR)
$atom_structure['pano'] = getid3_lib::BigEndian2Int(substr($atom_data, 0, 4));
case 'hint': // HINT track
$info['quicktime']['hinting'] = true;
case 'imgt': // IMaGe Track reference (kQTVRImageTrackRefType) (seen on QTVR)
for ($i = 0; $i < ($atom_structure['size'] - 8); $i += 4) {
$atom_structure['imgt'][] = getid3_lib::BigEndian2Int(substr($atom_data, $i, 4));
// Observed-but-not-handled atom types are just listed here to prevent warnings being generated
case 'FXTC': // Something to do with Adobe After Effects (?)
case 'FIEL': // this is NOT "fiel" (Field Ordering) as describe here: http://developer.apple.com/documentation/QuickTime/QTFF/QTFFChap3/chapter_4_section_2.html
case 'tapt': // TrackApertureModeDimensionsAID - http://developer.apple.com/documentation/QuickTime/Reference/QT7-1_Update_Reference/Constants/Constants.html
// tapt seems to be used to compute the video size [https://www.getid3.org/phpBB3/viewtopic.php?t=838]
// * http://lists.apple.com/archives/quicktime-api/2006/Aug/msg00014.html
// * http://handbrake.fr/irclogs/handbrake-dev/handbrake-dev20080128_pg2.html
case 'ctts':// STCompositionOffsetAID - http://developer.apple.com/documentation/QuickTime/Reference/QTRef_Constants/Reference/reference.html
case 'cslg':// STCompositionShiftLeastGreatestAID - http://developer.apple.com/documentation/QuickTime/Reference/QTRef_Constants/Reference/reference.html
case 'sdtp':// STSampleDependencyAID - http://developer.apple.com/documentation/QuickTime/Reference/QTRef_Constants/Reference/reference.html
case 'stps':// STPartialSyncSampleAID - http://developer.apple.com/documentation/QuickTime/Reference/QTRef_Constants/Reference/reference.html
//$atom_structure['data'] = $atom_data;
case "\xA9".'xyz': // GPS latitude+longitude+altitude
$atom_structure['data'] = $atom_data;
if (preg_match('#([\\+\\-][0-9\\.]+)([\\+\\-][0-9\\.]+)([\\+\\-][0-9\\.]+)?/$#i', $atom_data, $matches)) {
@list($all, $latitude, $longitude, $altitude) = $matches;
$info['quicktime']['comments']['gps_latitude'][] = floatval($latitude);
$info['quicktime']['comments']['gps_longitude'][] = floatval($longitude);
$info['quicktime']['comments']['gps_altitude'][] = floatval($altitude);
$this->warning('QuickTime atom "©xyz" data does not match expected data pattern at offset '.$baseoffset.'. Please report as getID3() bug.');
// https://exiftool.org/TagNames/Nikon.html
// Nikon-specific QuickTime tags found in the NCDT atom of MOV videos from some Nikon cameras such as the Coolpix S8000 and D5100
$atom_structure['subatoms'] = $this->QuicktimeParseContainerAtom($atom_data, $baseoffset + 4, $atomHierarchy, $ParseAllPossibleAtoms);
case 'NCTH': // Nikon Camera THumbnail image
case 'NCVW': // Nikon Camera preVieW image
case 'NCM1': // Nikon Camera preview iMage 1
case 'NCM2': // Nikon Camera preview iMage 2
// https://exiftool.org/TagNames/Nikon.html
if (preg_match('/^\xFF\xD8\xFF/', $atom_data)) {
'NCTH' => 'Nikon Camera Thumbnail Image',
'NCVW' => 'Nikon Camera Preview Image',
'NCM1' => 'Nikon Camera Preview Image 1',
'NCM2' => 'Nikon Camera Preview Image 2',
$atom_structure['data'] = $atom_data;
$atom_structure['image_mime'] = 'image/jpeg';
$atom_structure['description'] = $descriptions[$atomname];
$info['quicktime']['comments']['picture'][] = array(
'image_mime' => $atom_structure['image_mime'],
'description' => $atom_structure['description']
case 'NCTG': // Nikon - https://exiftool.org/TagNames/Nikon.html#NCTG
getid3_lib::IncludeDependency(GETID3_INCLUDEPATH.'module.tag.nikon-nctg.php', __FILE__, true);
$nikonNCTG = new getid3_tag_nikon_nctg($this->getid3);
$atom_structure['data'] = $nikonNCTG->parse($atom_data);
case 'NCHD': // Nikon:MakerNoteVersion - https://exiftool.org/TagNames/Nikon.html
for ($i = 0, $iMax = strlen($atom_data); $i < $iMax; ++$i) {
if (ord($atom_data[$i]) <= 0x1F) {
$makerNoteVersion .= ' '.ord($atom_data[$i]);
$makerNoteVersion .= $atom_data[$i];
$makerNoteVersion = rtrim($makerNoteVersion, "\x00");
$atom_structure['data'] = array(
'MakerNoteVersion' => $makerNoteVersion
case 'NCDB': // Nikon - https://exiftool.org/TagNames/Nikon.html
case 'CNCV': // Canon:CompressorVersion - https://exiftool.org/TagNames/Canon.html
$atom_structure['data'] = $atom_data;
// some kind of metacontainer, may contain a big data dump such as:
// mdta keys \005 mdtacom.apple.quicktime.make (mdtacom.apple.quicktime.creationdate ,mdtacom.apple.quicktime.location.ISO6709 $mdtacom.apple.quicktime.software !mdtacom.apple.quicktime.model ilst \01D \001 \015data \001DE\010Apple 0 \002 (data \001DE\0102011-05-11T17:54:04+0200 2 \003 *data \001DE\010+52.4936+013.3897+040.247/ \01D \004 \015data \001DE\0104.3.1 \005 \018data \001DE\010iPhone 4
// https://xhelmboyx.tripod.com/formats/qti-layout.txt
$atom_structure['version'] = getid3_lib::BigEndian2Int(substr($atom_data, 0, 1));
$atom_structure['flags_raw'] = getid3_lib::BigEndian2Int(substr($atom_data, 1, 3));
$atom_structure['subatoms'] = $this->QuicktimeParseContainerAtom(substr($atom_data, 4), $baseoffset + 8, $atomHierarchy, $ParseAllPossibleAtoms);
//$atom_structure['subatoms'] = $this->QuicktimeParseContainerAtom($atom_data, $baseoffset + 8, $atomHierarchy, $ParseAllPossibleAtoms);
case 'meta': // METAdata atom
// https://developer.apple.com/library/mac/documentation/QuickTime/QTFF/Metadata/Metadata.html
$atom_structure['version'] = getid3_lib::BigEndian2Int(substr($atom_data, 0, 1));
$atom_structure['flags_raw'] = getid3_lib::BigEndian2Int(substr($atom_data, 1, 3));
$atom_structure['subatoms'] = $this->QuicktimeParseContainerAtom($atom_data, $baseoffset + 8, $atomHierarchy, $ParseAllPossibleAtoms);
case 'data': // metaDATA atom
static $metaDATAkey = 1; // real ugly, but so is the QuickTime structure that stores keys and values in different multinested locations that are hard to relate to each other
// seems to be 2 bytes language code (ASCII), 2 bytes unknown (set to 0x10B5 in sample I have), remainder is useful data
$atom_structure['language'] = substr($atom_data, 4 + 0, 2);
$atom_structure['unknown'] = getid3_lib::BigEndian2Int(substr($atom_data, 4 + 2, 2));
$atom_structure['data'] = substr($atom_data, 4 + 4);
$atom_structure['key_name'] = (isset($info['quicktime']['temp_meta_key_names'][$metaDATAkey]) ? $info['quicktime']['temp_meta_key_names'][$metaDATAkey] : '');
if ($atom_structure['key_name'] && $atom_structure['data']) {
@$info['quicktime']['comments'][str_replace('com.apple.quicktime.', '', $atom_structure['key_name'])][] = $atom_structure['data'];
case 'keys': // KEYS that may be present in the metadata atom.
// https://developer.apple.com/library/mac/documentation/QuickTime/QTFF/Metadata/Metadata.html#//apple_ref/doc/uid/TP40000939-CH1-SW21
// The metadata item keys atom holds a list of the metadata keys that may be present in the metadata atom.
// This list is indexed starting with 1; 0 is a reserved index value. The metadata item keys atom is a full atom with an atom type of "keys".
$atom_structure['version'] = getid3_lib::BigEndian2Int(substr($atom_data, 0, 1));
$atom_structure['flags_raw'] = getid3_lib::BigEndian2Int(substr($atom_data, 1, 3));
$atom_structure['entry_count'] = getid3_lib::BigEndian2Int(substr($atom_data, 4, 4));
for ($i = 1; $i <= $atom_structure['entry_count']; $i++) {
$atom_structure['keys'][$i]['key_size'] = getid3_lib::BigEndian2Int(substr($atom_data, $keys_atom_offset + 0, 4));
$atom_structure['keys'][$i]['key_namespace'] = substr($atom_data, $keys_atom_offset + 4, 4);
$atom_structure['keys'][$i]['key_value'] = substr($atom_data, $keys_atom_offset + 8, $atom_structure['keys'][$i]['key_size'] - 8);
$keys_atom_offset += $atom_structure['keys'][$i]['key_size']; // key_size includes the 4+4 bytes for key_size and key_namespace
$info['quicktime']['temp_meta_key_names'][$i] = $atom_structure['keys'][$i]['key_value'];
case 'uuid': // user-defined atom often seen containing XML data, also used for potentially many other purposes, only a few specifically handled by getID3 (e.g. 360fly spatial data)
//Get the UUID ID in first 16 bytes
$uuid_bytes_read = unpack('H8time_low/H4time_mid/H4time_hi/H4clock_seq_hi/H12clock_seq_low', substr($atom_data, 0, 16));
$atom_structure['uuid_field_id'] = implode('-', $uuid_bytes_read);
switch ($atom_structure['uuid_field_id']) { // http://fileformats.archiveteam.org/wiki/Boxes/atoms_format#UUID_boxes
case '0537cdab-9d0c-4431-a72a-fa561f2a113e': // Exif - http://fileformats.archiveteam.org/wiki/Exif
case '2c4c0100-8504-40b9-a03e-562148d6dfeb': // Photoshop Image Resources - http://fileformats.archiveteam.org/wiki/Photoshop_Image_Resources
case '33c7a4d2-b81d-4723-a0ba-f1a3e097ad38': // IPTC-IIM - http://fileformats.archiveteam.org/wiki/IPTC-IIM
case '8974dbce-7be7-4c51-84f9-7148f9882554': // PIFF Track Encryption Box - http://fileformats.archiveteam.org/wiki/Protected_Interoperable_File_Format
case '96a9f1f1-dc98-402d-a7ae-d68e34451809': // GeoJP2 World File Box - http://fileformats.archiveteam.org/wiki/GeoJP2
case 'a2394f52-5a9b-4f14-a244-6c427c648df4': // PIFF Sample Encryption Box - http://fileformats.archiveteam.org/wiki/Protected_Interoperable_File_Format
case 'b14bf8bd-083d-4b43-a5ae-8cd7d5a6ce03': // GeoJP2 GeoTIFF Box - http://fileformats.archiveteam.org/wiki/GeoJP2
case 'd08a4f18-10f3-4a82-b6c8-32d8aba183d3': // PIFF Protection System Specific Header Box - http://fileformats.archiveteam.org/wiki/Protected_Interoperable_File_Format
$this->warning('Unhandled (but recognized) "uuid" atom identified by "'.$atom_structure['uuid_field_id'].'" at offset '.$atom_structure['offset'].' ('.strlen($atom_data).' bytes)');
case 'be7acfcb-97a9-42e8-9c71-999491e3afac': // XMP data (in XML format)
$atom_structure['xml'] = substr($atom_data, 16, strlen($atom_data) - 16 - 8); // 16 bytes for UUID, 8 bytes header(?)
case 'efe1589a-bb77-49ef-8095-27759eb1dc6f': // 360fly data
/* 360fly code in this block by Paul Lewis 2019-Oct-31 */
/* Sensor Timestamps need to be calculated using the recordings base time at ['quicktime']['moov']['subatoms'][0]['creation_time_unix']. */
$atom_structure['title'] = '360Fly Sensor Data';
//Get the UUID HEADER data
$uuid_bytes_read = unpack('vheader_size/vheader_version/vtimescale/vhardware_version/x/x/x/x/x/x/x/x/x/x/x/x/x/x/x/x/', substr($atom_data, 16, 32));
$atom_structure['uuid_header'] = $uuid_bytes_read;
$atom_SENSOR_data = substr($atom_data, $start_byte);
$atom_structure['sensor_data']['data_type'] = array(
'fusion_count' => 0, // ID 250
'fusion_data' => array(),
'accel_count' => 0, // ID 1
'gyro_count' => 0, // ID 2
'magno_count' => 0, // ID 3
'gps_count' => 0, // ID 5
'rotation_count' => 0, // ID 6
'rotation_data' => array(),
'unknown_count' => 0, // ID ??
'unknown_data' => array(),
'debug_list' => '', // Used to debug variables stored as comma delimited strings
$debug_structure = array();
$debug_structure['debug_items'] = array();
// Can start loop here to decode all sensor data in 32 Byte chunks:
foreach (str_split($atom_SENSOR_data, 32) as $sensor_key => $sensor_data) {
// This gets me a data_type code to work out what data is in the next 31 bytes.
$sensor_data_type = substr($sensor_data, 0, 1);
$sensor_data_content = substr($sensor_data, 1);
$uuid_bytes_read = unpack('C*', $sensor_data_type);
$sensor_data_array = array();
switch ($uuid_bytes_read[1]) {
$atom_structure['sensor_data']['data_type']['fusion_count']++;
$uuid_bytes_read = unpack('cmode/Jtimestamp/Gyaw/Gpitch/Groll/x*', $sensor_data_content);
$sensor_data_array['mode'] = $uuid_bytes_read['mode'];
$sensor_data_array['timestamp'] = $uuid_bytes_read['timestamp'];
$sensor_data_array['yaw'] = $uuid_bytes_read['yaw'];
$sensor_data_array['pitch'] = $uuid_bytes_read['pitch'];
$sensor_data_array['roll'] = $uuid_bytes_read['roll'];
array_push($atom_structure['sensor_data']['data_type']['fusion_data'], $sensor_data_array);
$atom_structure['sensor_data']['data_type']['accel_count']++;
$uuid_bytes_read = unpack('cmode/Jtimestamp/Gyaw/Gpitch/Groll/x*', $sensor_data_content);
$sensor_data_array['mode'] = $uuid_bytes_read['mode'];
$sensor_data_array['timestamp'] = $uuid_bytes_read['timestamp'];
$sensor_data_array['yaw'] = $uuid_bytes_read['yaw'];
$sensor_data_array['pitch'] = $uuid_bytes_read['pitch'];
$sensor_data_array['roll'] = $uuid_bytes_read['roll'];
array_push($atom_structure['sensor_data']['data_type']['accel_data'], $sensor_data_array);
$atom_structure['sensor_data']['data_type']['gyro_count']++;
$uuid_bytes_read = unpack('cmode/Jtimestamp/Gyaw/Gpitch/Groll/x*', $sensor_data_content);
$sensor_data_array['mode'] = $uuid_bytes_read['mode'];
$sensor_data_array['timestamp'] = $uuid_bytes_read['timestamp'];
$sensor_data_array['yaw'] = $uuid_bytes_read['yaw'];
$sensor_data_array['pitch'] = $uuid_bytes_read['pitch'];
$sensor_data_array['roll'] = $uuid_bytes_read['roll'];
array_push($atom_structure['sensor_data']['data_type']['gyro_data'], $sensor_data_array);
$atom_structure['sensor_data']['data_type']['magno_count']++;
$uuid_bytes_read = unpack('cmode/Jtimestamp/Gmagx/Gmagy/Gmagz/x*', $sensor_data_content);
$sensor_data_array['mode'] = $uuid_bytes_read['mode'];
$sensor_data_array['timestamp'] = $uuid_bytes_read['timestamp'];
$sensor_data_array['magx'] = $uuid_bytes_read['magx'];
$sensor_data_array['magy'] = $uuid_bytes_read['magy'];
$sensor_data_array['magz'] = $uuid_bytes_read['magz'];
array_push($atom_structure['sensor_data']['data_type']['magno_data'], $sensor_data_array);
$atom_structure['sensor_data']['data_type']['gps_count']++;
$uuid_bytes_read = unpack('cmode/Jtimestamp/Glat/Glon/Galt/Gspeed/nbearing/nacc/x*', $sensor_data_content);
$sensor_data_array['mode'] = $uuid_bytes_read['mode'];
$sensor_data_array['timestamp'] = $uuid_bytes_read['timestamp'];
$sensor_data_array['lat'] = $uuid_bytes_read['lat'];
$sensor_data_array['lon'] = $uuid_bytes_read['lon'];
$sensor_data_array['alt'] = $uuid_bytes_read['alt'];
$sensor_data_array['speed'] = $uuid_bytes_read['speed'];
$sensor_data_array['bearing'] = $uuid_bytes_read['bearing'];
$sensor_data_array['acc'] = $uuid_bytes_read['acc'];
array_push($atom_structure['sensor_data']['data_type']['gps_data'], $sensor_data_array);
//array_push($debug_structure['debug_items'], $uuid_bytes_read['timestamp']);
$atom_structure['sensor_data']['data_type']['rotation_count']++;
$uuid_bytes_read = unpack('cmode/Jtimestamp/Grotx/Groty/Grotz/x*', $sensor_data_content);
$sensor_data_array['mode'] = $uuid_bytes_read['mode'];
$sensor_data_array['timestamp'] = $uuid_bytes_read['timestamp'];
$sensor_data_array['rotx'] = $uuid_bytes_read['rotx'];
$sensor_data_array['roty'] = $uuid_bytes_read['roty'];
$sensor_data_array['rotz'] = $uuid_bytes_read['rotz'];
array_push($atom_structure['sensor_data']['data_type']['rotation_data'], $sensor_data_array);
$atom_structure['sensor_data']['data_type']['unknown_count']++;
//if (isset($debug_structure['debug_items']) && count($debug_structure['debug_items']) > 0) {
// $atom_structure['sensor_data']['data_type']['debug_list'] = implode(',', $debug_structure['debug_items']);
$atom_structure['sensor_data']['data_type']['debug_list'] = 'No debug items in list!';
$this->warning('Unhandled "uuid" atom identified by "'.$atom_structure['uuid_field_id'].'" at offset '.$atom_structure['offset'].' ('.strlen($atom_data).' bytes)');
// https://dashcamtalk.com/forum/threads/script-to-extract-gps-data-from-novatek-mp4.20808/page-2#post-291730
// The 'gps ' contains simple look up table made up of 8byte rows, that point to the 'free' atoms that contains the actual GPS data.
// The first row is version/metadata/notsure, I skip that.
// The following rows consist of 4byte address (absolute) and 4byte size (0x1000), these point to the GPS data in the file.
$GPS_rowsize = 8; // 4 bytes for offset, 4 bytes for size
if (strlen($atom_data) > 0) {
if ((strlen($atom_data) % $GPS_rowsize) == 0) {
$atom_structure['gps_toc'] = array();
foreach (str_split($atom_data, $GPS_rowsize) as $counter => $datapair) {
$atom_structure['gps_toc'][] = unpack('Noffset/Nsize', substr($atom_data, $counter * $GPS_rowsize, $GPS_rowsize));
$atom_structure['gps_entries'] = array();
$previous_offset = $this->ftell();
foreach ($atom_structure['gps_toc'] as $key => $gps_pointer) {
// "The first row is version/metadata/notsure, I skip that."
$this->fseek($gps_pointer['offset']);
$GPS_free_data = $this->fread($gps_pointer['size']);
// 2017-05-10: I see some of the data, notably the Hour-Minute-Second, but cannot reconcile the rest of the data. However, the NMEA "GPRMC" line is there and relatively easy to parse, so I'm using that instead
// https://dashcamtalk.com/forum/threads/script-to-extract-gps-data-from-novatek-mp4.20808/page-2#post-291730
// The structure of the GPS data atom (the 'free' atoms mentioned above) is following:
// hour,minute,second,year,month,day,active,latitude_b,longitude_b,unknown2,latitude,longitude,speed = struct.unpack_from('<IIIIIIssssfff',data, 48)
// For those unfamiliar with python struct:
// s = is string (size 1, in this case)
//$atom_structure['gps_entries'][$key] = unpack('Vhour/Vminute/Vsecond/Vyear/Vmonth/Vday/Vactive/Vlatitude_b/Vlongitude_b/Vunknown2/flatitude/flongitude/fspeed', substr($GPS_free_data, 48));
// $GPRMC,081836,A,3751.65,S,14507.36,E,000.0,360.0,130998,011.3,E*62
// $GPRMC,183731,A,3907.482,N,12102.436,W,000.0,360.0,080301,015.5,E*67
// $GPRMC,002454,A,3553.5295,N,13938.6570,E,0.0,43.1,180700,7.1,W,A*3F
// $GPRMC,094347.000,A,5342.0061,N,00737.9908,W,0.01,156.75,140217,,,A*7D
if (preg_match('#\\$GPRMC,([0-9\\.]*),([AV]),([0-9\\.]*),([NS]),([0-9\\.]*),([EW]),([0-9\\.]*),([0-9\\.]*),([0-9]*),([0-9\\.]*),([EW]?)(,[A])?(\\*[0-9A-F]{2})#', $GPS_free_data, $matches)) {
$GPS_this_GPRMC = array();
$GPS_this_GPRMC_raw = array();
$GPS_this_GPRMC_raw['gprmc'],
$GPS_this_GPRMC_raw['timestamp'],
$GPS_this_GPRMC_raw['status'],
$GPS_this_GPRMC_raw['latitude'],
$GPS_this_GPRMC_raw['latitude_direction'],
$GPS_this_GPRMC_raw['longitude'],
$GPS_this_GPRMC_raw['longitude_direction'],
$GPS_this_GPRMC_raw['knots'],
$GPS_this_GPRMC_raw['angle'],
$GPS_this_GPRMC_raw['datestamp'],
$GPS_this_GPRMC_raw['variation'],
$GPS_this_GPRMC_raw['variation_direction'],
$GPS_this_GPRMC_raw['checksum'],
$GPS_this_GPRMC['raw'] = $GPS_this_GPRMC_raw;
$hour = substr($GPS_this_GPRMC['raw']['timestamp'], 0, 2);
$minute = substr($GPS_this_GPRMC['raw']['timestamp'], 2, 2);
$second = substr($GPS_this_GPRMC['raw']['timestamp'], 4, 2);
$ms = substr($GPS_this_GPRMC['raw']['timestamp'], 6); // may contain decimal seconds
$day = substr($GPS_this_GPRMC['raw']['datestamp'], 0, 2);
$month = substr($GPS_this_GPRMC['raw']['datestamp'], 2, 2);
$year = (int) substr($GPS_this_GPRMC['raw']['datestamp'], 4, 2);
$year += (($year > 90) ? 1900 : 2000); // complete lack of foresight: datestamps are stored with 2-digit years, take best guess
$GPS_this_GPRMC['timestamp'] = $year.'-'.$month.'-'.$day.' '.$hour.':'.$minute.':'.$second.$ms;
$GPS_this_GPRMC['active'] = ($GPS_this_GPRMC['raw']['status'] == 'A'); // A=Active,V=Void
foreach (array('latitude','longitude') as $latlon) {
preg_match('#^([0-9]{1,3})([0-9]{2}\\.[0-9]+)$#', $GPS_this_GPRMC['raw'][$latlon], $matches);
list($dummy, $deg, $min) = $matches;
$GPS_this_GPRMC[$latlon] = $deg + ($min / 60);
$GPS_this_GPRMC['latitude'] *= (($GPS_this_GPRMC['raw']['latitude_direction'] == 'S') ? -1 : 1);
$GPS_this_GPRMC['longitude'] *= (($GPS_this_GPRMC['raw']['longitude_direction'] == 'W') ? -1 : 1);
$GPS_this_GPRMC['heading'] = $GPS_this_GPRMC['raw']['angle'];
$GPS_this_GPRMC['speed_knot'] = $GPS_this_GPRMC['raw']['knots'];
$GPS_this_GPRMC['speed_kmh'] = $GPS_this_GPRMC['raw']['knots'] * 1.852;
if ($GPS_this_GPRMC['raw']['variation']) {
$GPS_this_GPRMC['variation'] = $GPS_this_GPRMC['raw']['variation'];
$GPS_this_GPRMC['variation'] *= (($GPS_this_GPRMC['raw']['variation_direction'] == 'W') ? -1 : 1);
$atom_structure['gps_entries'][$key] = $GPS_this_GPRMC;
@$info['quicktime']['gps_track'][$GPS_this_GPRMC['timestamp']] = array(
'latitude' => (float) $GPS_this_GPRMC['latitude'],
'longitude' => (float) $GPS_this_GPRMC['longitude'],
'speed_kmh' => (float) $GPS_this_GPRMC['speed_kmh'],
'heading' => (float) $GPS_this_GPRMC['heading'],
$this->warning('Unhandled GPS format in "free" atom at offset '.$gps_pointer['offset']);