Skip to content

Commit e62f6b8

Browse files
feat(previews): previews for large remote files without full file download
Co-authored-by: Kate <26026535+provokateurin@users.noreply.github.com> Signed-off-by: invario <67800603+invario@users.noreply.github.com>
1 parent 8210e12 commit e62f6b8

File tree

1 file changed

+56
-14
lines changed

1 file changed

+56
-14
lines changed

lib/private/Preview/Movie.php

Lines changed: 56 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -54,22 +54,33 @@ public function getThumbnail(File $file, int $maxX, int $maxY): ?IImage {
5454

5555
$result = null;
5656
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
57+
// Try downloading 10 MB first, as it's likely that the first needed frames are present
58+
// there along with the 'moov atom" (used in MP4/MOV files). In some cases this doesn't
59+
// work, (e.g. the 'moov atom' is at the end, or the videos is high bitrate)
6160
if ($file->getStorage()->isLocal()) {
62-
$sizeAttempts = [5242880, null];
61+
// File is local, make two attempts: 10 MB, then the entire file
62+
$sizeAttempts = [10485760, null];
6363
} else {
64-
$sizeAttempts = [5242880];
64+
// File is remote, make one attempt: 10 MB will be downloaded from the file along with
65+
// 5 MB from the end with filler (null zeroes) in the middle.
66+
$sizeAttempts = [10485760];
6567
}
6668
} else {
6769
// size is irrelevant, only attempt once
6870
$sizeAttempts = [null];
6971
}
7072

7173
foreach ($sizeAttempts as $size) {
72-
$absPath = $this->getLocalFile($file, $size);
74+
$absPath = false;
75+
// File is remote, generate a sparse file
76+
if (!$file->getStorage()->isLocal()) {
77+
$absPath = $this->getSparseFile($file, $size);
78+
}
79+
80+
// Defaults to existing routine if generating sparse file fails
81+
if ($absPath === false) {
82+
$absPath = $this->getLocalFile($file, $size);
83+
}
7384
if ($absPath === false) {
7485
Server::get(LoggerInterface::class)->error(
7586
'Failed to get local file to generate thumbnail for: ' . $file->getPath(),
@@ -78,13 +89,8 @@ public function getThumbnail(File $file, int $maxX, int $maxY): ?IImage {
7889
return null;
7990
}
8091

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);
86-
}
87-
}
92+
// Attempt still image grab from 1 second and 0 second timestamp
93+
$result = $this->generateThumbNail($maxX, $maxY, $absPath, 1) ?? $this->generateThumbNail($maxX, $maxY, $absPath, 0);
8894

8995
$this->cleanTmpFiles();
9096

@@ -95,6 +101,42 @@ public function getThumbnail(File $file, int $maxX, int $maxY): ?IImage {
95101

96102
return $result;
97103
}
104+
105+
private function getSparseFile(File $file, int $size): string|false {
106+
$absPath = Server::get(ITempManager::class)->getTemporaryFile();
107+
if ($absPath === false) {
108+
Server::get(LoggerInterface::class)->error(
109+
'Failed to get sparse file to generate thumbnail for: ' . $file->getPath(),
110+
['app' => 'core']
111+
);
112+
return false;
113+
}
114+
$content = $file->fopen('r');
115+
116+
// Stream does not support seeking, so generating a sparse file is not possible
117+
if (stream_get_meta_data($content)['seekable'] === false) {
118+
fclose($content);
119+
return false;
120+
}
121+
122+
$sparseFile = fopen($absPath, 'w');
123+
// If filesize is small (i.e. <= $size + 5 MB) then just download entire file
124+
if (($size + 5242880) >= $file->getSize()) {
125+
stream_copy_to_stream($content, $sparseFile);
126+
} else {
127+
// Create a sparse file of equal size to original video
128+
ftruncate($sparseFile, $file->getSize());
129+
// Copy $size bytes to front end
130+
fseek($sparseFile, 0);
131+
stream_copy_to_stream($content, $sparseFile, $size, 0);
132+
// Copy 5 MB to tail end of file
133+
fseek($sparseFile, ($file->getSize() - 5242880));
134+
stream_copy_to_stream($content, $sparseFile, 5242880, ($file->getSize() - 5242880));
135+
}
136+
fclose($content);
137+
fclose($sparseFile);
138+
return $absPath;
139+
}
98140

99141
private function useHdr(string $absPath): bool {
100142
// load ffprobe path from configuration, otherwise generate binary path using ffmpeg binary path

0 commit comments

Comments
 (0)