Skip to content

Commit 7fede77

Browse files
invariocome-nc
andcommitted
feat(previews): allow ffmpeg to connect direct for AWS S3 buckets
Co-authored-by: Côme Chilliet <91878298+come-nc@users.noreply.github.com> Signed-off-by: invario <67800603+invario@users.noreply.github.com>
1 parent ec73f4a commit 7fede77

File tree

2 files changed

+87
-34
lines changed

2 files changed

+87
-34
lines changed

apps/files_external/lib/Lib/Storage/AmazonS3.php

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
<?php
2+
23
/**
34
* SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
45
* SPDX-FileCopyrightText: 2016 ownCloud, Inc.
@@ -756,4 +757,30 @@ public function writeStream(string $path, $stream, ?int $size = null): int {
756757

757758
return $size;
758759
}
760+
761+
/**
762+
* Generates and returns a presigned URL that expires after 1 minute.
763+
*
764+
*/
765+
public function getDirectDownload(string $path): array|false {
766+
$command = $this->getConnection()->getCommand('GetObject', [
767+
'Bucket' => $this->bucket,
768+
'Key' => $path,
769+
]);
770+
// generate a presigned URL that expires after 1 minute
771+
$request = $this->getConnection()->createPresignedRequest($command, '+1 minute', []);
772+
try {
773+
$presignedUrl = (string)$request->getUri();
774+
} catch (S3Exception $exception) {
775+
$this->logger->error($exception->getMessage(), [
776+
'app' => 'files_external',
777+
'exception' => $exception,
778+
]);
779+
}
780+
$result = [
781+
'url' => $presignedUrl,
782+
'presigned' => true,
783+
];
784+
return $result;
785+
}
759786
}

lib/private/Preview/Movie.php

Lines changed: 60 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,26 @@ public function isAvailable(FileInfo $file): bool {
4242
return is_string($this->binary);
4343
}
4444

45+
private function connectDirect(File $file): string|false {
46+
if (stream_get_meta_data($file->fopen('r'))['seekable'] !== true) {
47+
return false;
48+
}
49+
50+
// Checks for availability to access the video file directly via HTTP/HTTPS.
51+
// Returns a string containing URL if available. Only implemented and tested
52+
// with Amazon S3 currently. In all other cases, return false. ffmpeg
53+
// supports other protocols so this function may expand in the future.
54+
$gddValues = $file->getStorage()->getDirectDownload($file->getName());
55+
56+
if (is_array($gddValues)) {
57+
if (array_key_exists('url', $gddValues) && array_key_exists('presigned', $gddValues)) {
58+
$directUrl = (str_starts_with($gddValues['url'], 'http') && ($gddValues['presigned'] === true)) ? $gddValues['url'] : false;
59+
return $directUrl;
60+
}
61+
}
62+
return false;
63+
}
64+
4565
/**
4666
* {@inheritDoc}
4767
*/
@@ -51,54 +71,60 @@ public function getThumbnail(File $file, int $maxX, int $maxY): ?IImage {
5171
if (!$this->isAvailable($file)) {
5272
return null;
5373
}
54-
5574
$result = null;
56-
if ($this->useTempFile($file)) {
57-
// Try downloading 5 MB first, as it's likely that the first frames are present there.
58-
// In some cases this doesn't work, for example when the moov atom is at the
59-
// end of the file, so if it fails we fall back to getting the full file.
60-
// Unless the file is not local (e.g. S3) as we do not want to download the whole (e.g. 37Gb) file
61-
if ($file->getStorage()->isLocal()) {
62-
$sizeAttempts = [5242880, null];
75+
$connectDirect = $this->connectDirect($file);
76+
if (($connectDirect === false) || $file->isEncrypted()) {
77+
// If HTTP/HTTPS direct connect is not available or if the file is encrypted,
78+
// process normally with temp files
79+
if ($this->useTempFile($file)) {
80+
// Try downloading 5 MB first, as it's likely that the first frames are
81+
// present there. In some cases this doesn't work (e.g. when the
82+
// moov atom is at the end) so if it fails, fall back to
83+
// getting the full file, unless the file is not local as we do not want
84+
// to download the whole (e.g. 37GB) file from remote.
85+
if ($file->getStorage()->isLocal()) {
86+
$sizeAttempts = [5242880, null];
87+
} else {
88+
$sizeAttempts = [5242880];
89+
}
6390
} else {
64-
$sizeAttempts = [5242880];
91+
// size is irrelevant, only attempt once
92+
$sizeAttempts = [null];
6593
}
66-
} else {
67-
// size is irrelevant, only attempt once
68-
$sizeAttempts = [null];
69-
}
7094

71-
foreach ($sizeAttempts as $size) {
72-
$absPath = $this->getLocalFile($file, $size);
73-
if ($absPath === false) {
74-
Server::get(LoggerInterface::class)->error(
75-
'Failed to get local file to generate thumbnail for: ' . $file->getPath(),
76-
['app' => 'core']
77-
);
78-
return null;
79-
}
80-
81-
$result = $this->generateThumbNail($maxX, $maxY, $absPath, 5);
82-
if ($result === null) {
83-
$result = $this->generateThumbNail($maxX, $maxY, $absPath, 1);
84-
if ($result === null) {
85-
$result = $this->generateThumbNail($maxX, $maxY, $absPath, 0);
95+
foreach ($sizeAttempts as $size) {
96+
$absPath = $this->getLocalFile($file, $size);
97+
if ($absPath === false) {
98+
Server::get(LoggerInterface::class)->error(
99+
'Failed to get local file to generate thumbnail for: '
100+
. $file->getPath(), ['app' => 'core']
101+
);
102+
return null;
86103
}
87-
}
88104

89-
$this->cleanTmpFiles();
105+
$result = ($this->generateThumbNail($maxX, $maxY, $absPath, 5))
106+
?? ($this->generateThumbNail($maxX, $maxY, $absPath, 1))
107+
?? ($this->generateThumbNail($maxX, $maxY, $absPath, 0));
90108

91-
if ($result !== null) {
92-
break;
109+
$this->cleanTmpFiles();
110+
111+
if ($result !== null) {
112+
break;
113+
}
93114
}
115+
} else {
116+
// HTTP/HTTPS direct connect is available so pass the URL directly to ffmpeg
117+
$result = ($this->generateThumbNail($maxX, $maxY, $connectDirect, 5))
118+
?? ($this->generateThumbNail($maxX, $maxY, $connectDirect, 1))
119+
?? ($this->generateThumbNail($maxX, $maxY, $connectDirect, 0));
94120
}
95-
96121
return $result;
97122
}
98123

99124
private function useHdr(string $absPath): bool {
100125
// load ffprobe path from configuration, otherwise generate binary path using ffmpeg binary path
101-
$ffprobe_binary = $this->config->getSystemValue('preview_ffprobe_path', null) ?? (pathinfo($this->binary, PATHINFO_DIRNAME) . '/ffprobe');
126+
$ffprobe_binary = ($this->config->getSystemValue('preview_ffprobe_path', null))
127+
?? (pathinfo($this->binary, PATHINFO_DIRNAME) . '/ffprobe');
102128
// run ffprobe on the video file to get value of "color_transfer"
103129
$test_hdr_cmd = [$ffprobe_binary,'-select_streams', 'v:0',
104130
'-show_entries', 'stream=color_transfer',

0 commit comments

Comments
 (0)