@@ -54,22 +54,30 @@ 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+ if ($ file ->getStorage ()->isLocal ()) {
75+ // File is local
76+ $ absPath = $ this ->getLocalFile ($ file , $ size );
77+ } else {
78+ // File is remote, generate a sparse file
79+ $ absPath = $ this ->getSparseFile ($ file , $ size );
80+ }
7381 if ($ absPath === false ) {
7482 Server::get (LoggerInterface::class)->error (
7583 'Failed to get local file to generate thumbnail for: ' . $ file ->getPath (),
@@ -78,13 +86,8 @@ public function getThumbnail(File $file, int $maxX, int $maxY): ?IImage {
7886 return null ;
7987 }
8088
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- }
89+ // Attempt still image grab from 1 second and 0 second timestamp
90+ $ result = $ this ->generateThumbNail ($ maxX , $ maxY , $ absPath , 1 ) ?? $ this ->generateThumbNail ($ maxX , $ maxY , $ absPath , 0 );
8891
8992 $ this ->cleanTmpFiles ();
9093
@@ -95,6 +98,38 @@ public function getThumbnail(File $file, int $maxX, int $maxY): ?IImage {
9598
9699 return $ result ;
97100 }
101+
102+ private function getSparseFile (File $ file , int $ size ): string |false {
103+
104+ $ absPath = Server::get (ITempManager::class)->getTemporaryFile ();
105+ if ($ absPath === false ) {
106+ Server::get (LoggerInterface::class)->error (
107+ 'Failed to get sparse file to generate thumbnail for: ' . $ file ->getPath (),
108+ ['app ' => 'core ' ]
109+ );
110+ return false ;
111+ }
112+ $ sparseFile = fopen ($ absPath ,'w ' );
113+ $ content = $ file ->fopen ('r ' );
114+
115+ // If filesize is small (i.e. <= $size + 5 MB) then just download entire file
116+ if (($ size +5242880 ) >= $ file ->getSize ()) {
117+ stream_copy_to_stream ($ content , $ sparseFile );
118+ } else {
119+ // Create a sparse file of equal size to original video
120+ ftruncate ($ sparseFile , $ file ->getSize ());
121+ // Copy $size bytes to front end
122+ fseek ($ sparseFile , 0 );
123+ stream_copy_to_stream ($ content , $ sparseFile , $ size , 0 );
124+ // Copy 5 MB to tail end of file
125+ fseek ($ sparseFile , ($ file ->getSize ()-5242880 ));
126+ stream_copy_to_stream ($ content , $ sparseFile , 5242880 , ($ file ->getSize ()-5242880 ));
127+ }
128+
129+ fclose ($ sparseFile );
130+ fclose ($ content );
131+ return $ absPath ;
132+ }
98133
99134 private function useHdr (string $ absPath ): bool {
100135 // load ffprobe path from configuration, otherwise generate binary path using ffmpeg binary path
0 commit comments