@@ -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