diff --git a/lib/Controller/VideoController.php b/lib/Controller/VideoController.php index 2dd77f54..f716cb35 100644 --- a/lib/Controller/VideoController.php +++ b/lib/Controller/VideoController.php @@ -23,6 +23,7 @@ declare(strict_types=1); namespace OCA\Memories\Controller; +use OCA\Memories\Exif; use OCP\AppFramework\Http; use OCP\AppFramework\Http\DataDisplayResponse; use OCP\AppFramework\Http\JSONResponse; @@ -163,27 +164,60 @@ class VideoController extends ApiBase if (!$liveid) { return new JSONResponse(['message' => 'Live ID not provided'], Http::STATUS_BAD_REQUEST); } - $lp = $this->timelineQuery->getLivePhoto($fileid); - if (!$lp || $lp['liveid'] !== $liveid) { - return new JSONResponse(['message' => 'Live ID not found'], Http::STATUS_NOT_FOUND); + + // Response data + $name = ''; + $blob = null; + $mime = ''; + + // Video is inside the file + $path = null; + if (str_starts_with($liveid, 'self__')) { + $path = $file->getStorage()->getLocalFile($file->getInternalPath()); + $mime = 'video/mp4'; + $name = $file->getName().'.mp4'; } - // Get and return file - $liveFileId = (int) $lp['fileid']; - $files = $this->rootFolder->getById($liveFileId); - if (0 === \count($files)) { - return new JSONResponse(['message' => 'Live file not found'], Http::STATUS_NOT_FOUND); + // Different manufacurers have different formats + if ('self__trailer' === $liveid) { + try { // Get trailer + $blob = Exif::getBinaryExifProp($path, '-trailer'); + } catch (\Exception $e) { + return new JSONResponse(['message' => 'Trailer not found'], Http::STATUS_NOT_FOUND); + } + } elseif ('self__embeddedvideo' === $liveid) { + try { // Get embedded video file + $blob = Exif::getBinaryExifProp($path, '-EmbeddedVideoFile'); + } catch (\Exception $e) { + return new JSONResponse(['message' => 'Embedded video not found'], Http::STATUS_NOT_FOUND); + } + } else { + // Get stored video file (Apple MOV) + $lp = $this->timelineQuery->getLivePhoto($fileid); + if (!$lp || $lp['liveid'] !== $liveid) { + return new JSONResponse(['message' => 'Live ID not found'], Http::STATUS_NOT_FOUND); + } + + // Get and return file + $liveFileId = (int) $lp['fileid']; + $files = $this->rootFolder->getById($liveFileId); + if (0 === \count($files)) { + return new JSONResponse(['message' => 'Live file not found'], Http::STATUS_NOT_FOUND); + } + $liveFile = $files[0]; + + if ($liveFile instanceof File) { + $name = $liveFile->getName(); + $blob = $liveFile->getContent(); + $mime = $liveFile->getMimeType(); + } } - $liveFile = $files[0]; - - if ($liveFile instanceof File) { - // Create and send response - $name = $liveFile->getName(); - $blob = $liveFile->getContent(); + // Make and send response + if ($blob) { $response = new DataDisplayResponse($blob, Http::STATUS_OK, []); $response->setHeaders([ - 'Content-Type' => $liveFile->getMimeType(), + 'Content-Type' => $mime, 'Content-Disposition' => "attachment; filename=\"{$name}\"", ]); $response->cacheFor(3600 * 24, false, false); diff --git a/lib/Db/LivePhoto.php b/lib/Db/LivePhoto.php index 6476a178..074a619c 100644 --- a/lib/Db/LivePhoto.php +++ b/lib/Db/LivePhoto.php @@ -26,10 +26,28 @@ class LivePhoto /** Get liveid from photo part */ public function getLivePhotoId(array &$exif) { + // Apple JPEG (MOV has ContentIdentifier) if (\array_key_exists('MediaGroupUUID', $exif)) { return $exif['MediaGroupUUID']; } + // Samsung JPEG + if (\array_key_exists('EmbeddedVideoType', $exif) && str_contains($exif['EmbeddedVideoType'], 'MotionPhoto')) { + return 'self__embeddedvideo'; + } + + // Google JPEG and Samsung HEIC (Apple?) + if (\array_key_exists('MotionPhoto', $exif)) { + if ('image/jpeg' === $exif['MIMEType']) { + // Google JPEG -- image should hopefully be in trailer + return 'self__trailer'; + } + if ('image/heic' === $exif['MIMEType']) { + // Samsung HEIC -- no way to get this out yet + return ''; + } + } + return ''; } diff --git a/lib/Db/TimelineWrite.php b/lib/Db/TimelineWrite.php index 7aa3dccd..927f048f 100644 --- a/lib/Db/TimelineWrite.php +++ b/lib/Db/TimelineWrite.php @@ -134,7 +134,7 @@ class TimelineWrite } // These are huge and not needed - if (str_starts_with($key, 'Nikon')) { + if (str_starts_with($key, 'Nikon') || str_starts_with($key, 'QuickTime')) { unset($exif[$key]); } } diff --git a/lib/Exif.php b/lib/Exif.php index ebef819b..1388ceea 100644 --- a/lib/Exif.php +++ b/lib/Exif.php @@ -262,6 +262,28 @@ class Exif } } + public static function getBinaryExifProp(string $path, string $prop) + { + $pipes = []; + $proc = proc_open(array_merge(self::getExiftool(), [$prop, '-n', '-b', $path]), [ + 1 => ['pipe', 'w'], + 2 => ['pipe', 'w'], + ], $pipes); + stream_set_blocking($pipes[1], false); + + try { + return self::readOrTimeout($pipes[1], 5000); + } catch (\Exception $ex) { + error_log("Exiftool timeout: [{$path}]"); + + throw new \Exception('Could not read from Exiftool'); + } finally { + fclose($pipes[1]); + fclose($pipes[2]); + proc_terminate($proc); + } + } + /** Get path to exiftool binary */ private static function getExiftool() { diff --git a/src/App.vue b/src/App.vue index bd0506f5..fa09f584 100644 --- a/src/App.vue +++ b/src/App.vue @@ -358,7 +358,7 @@ aside.app-sidebar { } :root { - --livephoto-img-transition: opacity 0.5s linear, transform 0.4s ease-in-out; + --livephoto-img-transition: opacity 0.4s linear, transform 0.3s ease-in-out; } // Live photo transitions @@ -388,7 +388,7 @@ aside.app-sidebar { opacity: 1; } &.playing.canplay img { - transform: scale(1.07); + transform: scale(1.05); } }