oDb = &$oDb; $this->oProject = &$oProject; $this->asMedia = array(); $this->asMedias = array(); $this->sSystemType = (substr(php_uname(), 0, 7) == "Windows")?'win':'unix'; $this->setMediaId($iMediaId); } public function setMediaId($iMediaId) { $this->iMediaId = $iMediaId; } public function getMediaId() { return $this->iMediaId; } public function getProjectCodeName() { return $this->oProject->getProjectCodeName(); } public function setComment($sComment) { $sError = ''; $asData = array(); if($this->iMediaId > 0) { $bResult = $this->oDb->updateRow(self::MEDIA_TABLE, $this->iMediaId, array('comment'=>$sComment)); if(!$bResult) $sError = 'error_commit_db'; else $asData = $this->getInfo(); } else $sError = 'media_no_id'; return Spot::getResult(($sError==''), $sError, $asData); } public function getMediasInfo($oMediaIds=null) { $bOwnMedia = is_numeric($oMediaIds); //1 value $bConstraintArray = is_array($oMediaIds); //Custom Constraints if($bOwnMedia && empty($this->asMedia) || !$bOwnMedia && empty($this->asMedias) || $bConstraintArray) { if($this->oProject->getProjectId()) { $asParams = array( 'select' => array(Db::getId(self::MEDIA_TABLE), 'filename', 'taken_on', 'posted_on', 'timezone', 'latitude', 'longitude', 'width', 'height', 'rotate', 'type AS subtype', 'comment'), 'from' => self::MEDIA_TABLE, 'constraint'=> array(Db::getId(Project::PROJ_TABLE) => $this->oProject->getProjectId()) ); if($bOwnMedia) $asParams['constraint'][Db::getId(self::MEDIA_TABLE)] = $oMediaIds; if($bConstraintArray) $asParams = array_merge($asParams, $oMediaIds); $asMedias = $this->oDb->selectRows($asParams); foreach($asMedias as &$asMedia) { $asMedia['media_path'] = self::getMediaPath($asMedia['filename']); $asMedia['thumb_path'] = $this->getMediaThumbnail($asMedia['filename']); } if(!empty($asMedias) && !$bConstraintArray) { if($bOwnMedia) $this->asMedia = array_shift($asMedias); else $this->asMedias = $asMedias; } } } return $bOwnMedia?$this->asMedia:$asMedias; } public function getInfo() { return $this->getMediasInfo($this->iMediaId); } public function isProjectEditable() { return $this->oProject->isEditable(); } public function addMedia($sMediaName, $sMethod='upload') { $sError = ''; $asParams = array(); if(!$this->isProjectEditable() && $sMethod!='sync') { $sError = 'upload_mode_archived'; $asParams[] = $this->oProject->getProjectCodeName(); } elseif($this->oDb->pingValue(self::MEDIA_TABLE, array('filename'=>$sMediaName)) && $sMethod!='sync') { $sError = 'upload_media_exist'; $asParams[] = $sMediaName; } else { $asMediaInfo = $this->getMediaInfoFromFile($sMediaName); //Converting times to Site Time Zone, by using date() //Media Timezone is kept in a separate field for later conversion to Local Time $asDbInfo = array( Db::getId(Project::PROJ_TABLE) => $this->oProject->getProjectId(), 'filename' => $sMediaName, 'taken_on' => date(Db::TIMESTAMP_FORMAT, ($asMediaInfo['taken_ts'] > 0)?$asMediaInfo['taken_ts']:$asMediaInfo['file_ts']), 'posted_on' => date(Db::TIMESTAMP_FORMAT, $asMediaInfo['file_ts']), 'timezone' => $asMediaInfo['timezone'], 'latitude' => $asMediaInfo['latitude'], 'longitude' => $asMediaInfo['longitude'], 'altitude' => $asMediaInfo['altitude'], 'width' => $asMediaInfo['width'], 'height' => $asMediaInfo['height'], 'rotate' => $asMediaInfo['rotate'], 'type' => $asMediaInfo['type'] ); if($sMethod=='sync') $iMediaId = $this->oDb->insertUpdateRow(self::MEDIA_TABLE, $asDbInfo, array('filename')); else $iMediaId = $this->oDb->insertRow(self::MEDIA_TABLE, $asDbInfo); if(!$iMediaId) $sError = 'error_commit_db'; else { $this->setMediaId($iMediaId); $asParams = $this->getInfo(); //Creates thumbnail } } return Spot::getResult(($sError==''), $sError, $asParams); } private function getMediaInfoFromFile($sMediaName) { $sMediaPath = self::getMediaPath($sMediaName); $sType = self::getMediaType($sMediaName); $iPostedOn = filemtime($sMediaPath); $sTimeZone = date_default_timezone_get(); $iWidth = 0; $iHeight = 0; $sRotate = '0'; $sTakenOn = ''; $fLat = 0; $fLng = 0; $iAlt = 0; switch($sType) { case 'video': $asResult = array(); $sParams = implode(' ', array( '-loglevel error', //Remove comments '-select_streams v:0', //First video channel '-show_entries '. //filter tags : Width, Height, Creation Time, Location & Rotation 'format_tags=creation_time,com.apple.quicktime.creationdate,com.apple.quicktime.location.ISO6709:'. 'stream_tags=rotate,creation_time:'. 'stream=width,height', '-print_format json', //output format: json '-i' //input file )); exec('ffprobe '.$sParams.' "'.$sMediaPath.'"', $asResult); $asExif = json_decode(implode('', $asResult), true); //Taken On if(isset($asExif['format']['tags']['com.apple.quicktime.creationdate'])) { $sTakenOn = $asExif['format']['tags']['com.apple.quicktime.creationdate']; //contains Time Zone $sTimeZone = Spot::getTimeZoneFromDate($sTakenOn) ?? $sTimeZone; } else $sTakenOn = $asExif['format']['tags']['creation_time'] ?? $asExif['streams'][0]['tags']['creation_time']; //Location if(isset($asExif['format']['tags']['com.apple.quicktime.location.ISO6709'])) { list($fLat, $fLng, $iAlt) = self::getLatLngAltFromISO6709($asExif['format']['tags']['com.apple.quicktime.location.ISO6709']); } //Width & Height $iWidth = $asExif['streams'][0]['width']; $iHeight = $asExif['streams'][0]['height']; //Orientation if(isset($asExif['streams'][0]['tags']['rotate'])) $sRotate = $asExif['streams'][0]['tags']['rotate']; break; case 'image': $asExif = @exif_read_data($sMediaPath, 0, true); if($asExif === false) $asExif = array(); list($iWidth, $iHeight) = getimagesize($sMediaPath); //Posted On if(array_key_exists('FILE', $asExif) && array_key_exists('FileDateTime', $asExif['FILE'])) $iPostedOn = $asExif['FILE']['FileDateTime']; //Taken On & Timezone if(array_key_exists('EXIF', $asExif)) { if(array_key_exists('DateTimeOriginal', $asExif['EXIF'])) $sTakenOn = $asExif['EXIF']['DateTimeOriginal']; /* Priorities: * 1. OffsetTimeOriginal: timezone for DateTimeOriginal (exif version >= 2.31) * 2. 0x9011: same as above, but unidentified * 3. Timezone extracted from DateTimeOriginal * 4. Uploader Browser Time Zone (PHP Default Time Zone) */ $sTimeZone = $asExif['EXIF']['OffsetTimeOriginal'] ?? $asExif['EXIF']['UndefinedTag:0x9011'] ?? Spot::getTimeZoneFromDate($sTakenOn) ?? $sTimeZone; } //Location if(array_key_exists('GPS', $asExif)) { $asGps = $asExif['GPS']; $fLat = self::getLatLngFromExif($asGps['GPSLatitudeRef'] ?? $asGps['UndefinedTag:0x0001'], $asGps['GPSLatitude'] ?? $asGps['UndefinedTag:0x0002']); $fLng = self::getLatLngFromExif($asGps['GPSLongitudeRef'] ?? $asGps['UndefinedTag:0x0003'], $asGps['GPSLongitude'] ?? $$asGps['UndefinedTag:0x0004']); $iAlt = (($asGps['GPSAltitudeRef'] ?? $asGps['UndefinedTag:0x0005'] ?? '0') == '1'?-1:1) * ($asGps['GPSAltitude'] ?? $asGps['UndefinedTag:0x0006'] ?? 0); } //Orientation if(array_key_exists('IFD0', $asExif) && array_key_exists('Orientation', $asExif['IFD0'])) { switch($asExif['IFD0']['Orientation']) { case 1: $sRotate = '0'; break; //None case 3: $sRotate = '180'; break; //Flip over case 6: $sRotate = '90'; break; //Clockwise case 8: $sRotate = '-90'; break; //Trigo } } break; } //Assign the correct Time Zone to $sTakenOn if it is not already contained in it. Then get Unix Timestamp //Time Zone (2nd parameter) will be ignored if already contained in $sTakenOn $iTakenOn = 0; if($sTakenOn != '') { $oTakenOn = new \DateTime($sTakenOn, new \DateTimeZone($sTimeZone)); $iTakenOn = $oTakenOn->format('U'); } return array( 'timezone' => $sTimeZone, 'latitude' => $fLat, 'longitude' => $fLng, 'altitude' => $iAlt, 'taken_ts' => $iTakenOn, 'file_ts' => $iPostedOn, 'width' => $iWidth, 'height' => $iHeight, 'rotate' => $sRotate, 'type' => $sType ); } private function getMediaThumbnail($sMediaName) { $sMediaPath = self::getMediaPath($sMediaName); $sThumbPath = self::getMediaPath($sMediaName, 'thumbnail'); if(!file_exists($sThumbPath)) { $sType = self::getMediaType($sMediaName); switch($sType) { case 'image': $asThumbInfo = ToolBox::createThumbnail($sMediaPath, self::THUMB_MAX_WIDTH, 0, $sThumbPath); break; case 'video': //Get a screenshot of the video 1 second in $sTempPath = self::getMediaPath(uniqid('temp_').'.png'); $asResult = array(); $sParams = implode(' ', array( '-i "'.$sMediaPath.'"', //input file '-ss 00:00:01.000', //Image taken after x seconds '-vframes 1', //number of video frames to output '"'.$sTempPath.'"', //output file )); exec('ffmpeg '.$sParams, $asResult); //Resize $asThumbInfo = ToolBox::createThumbnail($sTempPath, self::THUMB_MAX_WIDTH, 0, $sThumbPath, true); break; } } else $asThumbInfo = array('error'=>'', 'out'=>$sThumbPath); return ($asThumbInfo['error']=='')?$asThumbInfo['out']:$sMediaPath; } private static function getMediaPath($sMediaName, $sFileType='media') { if($sFileType=='thumbnail') return self::THUMB_FOLDER.$sMediaName.(strtolower(substr($sMediaName, -3))=='mov'?'.png':''); else return self::MEDIA_FOLDER.$sMediaName; } private static function getMediaType($sMediaName) { $sMediaPath = self::getMediaPath($sMediaName); $sMediaMime = mime_content_type($sMediaPath); switch($sMediaMime) { case 'video/quicktime': $sType = 'video'; break; default: $sType = 'image'; break; } return $sType; } private static function getLatLngFromExif($sDirection, $asCoords) { $fCoord = 0; foreach($asCoords as $iIndex=>$sCoord) { $fValue = 0; $asCoordParts = explode('/', $sCoord); switch(count($asCoordParts)) { case 1: $fValue = $asCoordParts[0]; break; case 2: $fValue = floatval($asCoordParts[0]) / floatval($asCoordParts[1]); break; } $fCoord += $fValue / pow(60, $iIndex); } return $fCoord * (($sDirection == 'W' || $sDirection == 'S')?-1:1); } private static function getLatLngAltFromISO6709($sIso6709) { preg_match('/^(?P[\+\-][0,1]?\d{2}\.\d+)(?P[\+\-][0,1]?\d{2}\.\d+)(?P[\+\-]\d+)?/', $sIso6709, $asMatches); return array(floatval($asMatches['lat']), floatval($asMatches['lng']), floatval($asMatches['alt'] ?? 0)); } }