livephoto: add Google and Samsung support
parent
578703768b
commit
799a39f968
|
@ -23,6 +23,7 @@ declare(strict_types=1);
|
||||||
|
|
||||||
namespace OCA\Memories\Controller;
|
namespace OCA\Memories\Controller;
|
||||||
|
|
||||||
|
use OCA\Memories\Exif;
|
||||||
use OCP\AppFramework\Http;
|
use OCP\AppFramework\Http;
|
||||||
use OCP\AppFramework\Http\DataDisplayResponse;
|
use OCP\AppFramework\Http\DataDisplayResponse;
|
||||||
use OCP\AppFramework\Http\JSONResponse;
|
use OCP\AppFramework\Http\JSONResponse;
|
||||||
|
@ -163,27 +164,60 @@ class VideoController extends ApiBase
|
||||||
if (!$liveid) {
|
if (!$liveid) {
|
||||||
return new JSONResponse(['message' => 'Live ID not provided'], Http::STATUS_BAD_REQUEST);
|
return new JSONResponse(['message' => 'Live ID not provided'], Http::STATUS_BAD_REQUEST);
|
||||||
}
|
}
|
||||||
$lp = $this->timelineQuery->getLivePhoto($fileid);
|
|
||||||
if (!$lp || $lp['liveid'] !== $liveid) {
|
// Response data
|
||||||
return new JSONResponse(['message' => 'Live ID not found'], Http::STATUS_NOT_FOUND);
|
$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
|
// Different manufacurers have different formats
|
||||||
$liveFileId = (int) $lp['fileid'];
|
if ('self__trailer' === $liveid) {
|
||||||
$files = $this->rootFolder->getById($liveFileId);
|
try { // Get trailer
|
||||||
if (0 === \count($files)) {
|
$blob = Exif::getBinaryExifProp($path, '-trailer');
|
||||||
return new JSONResponse(['message' => 'Live file not found'], Http::STATUS_NOT_FOUND);
|
} 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 = new DataDisplayResponse($blob, Http::STATUS_OK, []);
|
||||||
$response->setHeaders([
|
$response->setHeaders([
|
||||||
'Content-Type' => $liveFile->getMimeType(),
|
'Content-Type' => $mime,
|
||||||
'Content-Disposition' => "attachment; filename=\"{$name}\"",
|
'Content-Disposition' => "attachment; filename=\"{$name}\"",
|
||||||
]);
|
]);
|
||||||
$response->cacheFor(3600 * 24, false, false);
|
$response->cacheFor(3600 * 24, false, false);
|
||||||
|
|
|
@ -26,10 +26,28 @@ class LivePhoto
|
||||||
/** Get liveid from photo part */
|
/** Get liveid from photo part */
|
||||||
public function getLivePhotoId(array &$exif)
|
public function getLivePhotoId(array &$exif)
|
||||||
{
|
{
|
||||||
|
// Apple JPEG (MOV has ContentIdentifier)
|
||||||
if (\array_key_exists('MediaGroupUUID', $exif)) {
|
if (\array_key_exists('MediaGroupUUID', $exif)) {
|
||||||
return $exif['MediaGroupUUID'];
|
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 '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -134,7 +134,7 @@ class TimelineWrite
|
||||||
}
|
}
|
||||||
|
|
||||||
// These are huge and not needed
|
// 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]);
|
unset($exif[$key]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
22
lib/Exif.php
22
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 */
|
/** Get path to exiftool binary */
|
||||||
private static function getExiftool()
|
private static function getExiftool()
|
||||||
{
|
{
|
||||||
|
|
|
@ -358,7 +358,7 @@ aside.app-sidebar {
|
||||||
}
|
}
|
||||||
|
|
||||||
:root {
|
: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
|
// Live photo transitions
|
||||||
|
@ -388,7 +388,7 @@ aside.app-sidebar {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
&.playing.canplay img {
|
&.playing.canplay img {
|
||||||
transform: scale(1.07);
|
transform: scale(1.05);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
Loading…
Reference in New Issue