@@ -53,23 +53,39 @@ public function getThumbnail(File $file, int $maxX, int $maxY): ?IImage {
5353		}
5454
5555		$ resultnull ;
56+ 
57+ 		// Timestamps to make attempts to generate a still 
58+ 		$ timeAttempts5 , 1 , 0 ];
59+ 
60+ 		// By default, download $sizeAttempts from the file along with 
61+ 		// the 'moov' atom. 
62+ 		// Example bitrates in the higher range: 
63+ 		// 4K HDR H265 60 FPS = 75 Mbps = 9 MB per second needed for a still 
64+ 		// 1080p H265 30 FPS = 10 Mbps = 1.25 MB per second needed for a still 
65+ 		// 1080p H264 30 FPS = 16 Mbps = 2 MB per second needed for a still 
66+ 		$ sizeAttempts1024  * 1024  * 10 ];
67+ 
5668		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 
6169			if  ($ filegetStorage ()->isLocal ()) {
62- 				$ sizeAttempts  = [ 5242880 ,  null ]; 
63- 			}  else  { 
64- 				$ sizeAttempts[ 5242880 ] ;
70+ 				// temp file required but local, so retrieve $sizeAttempt first, then 
71+ 				 // the entire file 
72+ 				$ sizeAttempts[]  = null ;
6573			}
6674		} else  {
67- 			// size  is irrelevant, only attempt once  
75+ 			// temp not required and file  is local so retrieve entire file  
6876			$ sizeAttemptsnull ];
6977		}
7078
7179		foreach  ($ sizeAttemptsas  $ size
72- 			$ absPath$ this getLocalFile ($ file$ size
80+ 			$ absPathfalse ;
81+ 			// File is remote, generate a sparse file 
82+ 			if  (!$ filegetStorage ()->isLocal ()) {
83+ 				$ absPath$ this getSparseFile ($ file$ size
84+ 			}
85+ 			// Defaults to existing routine if generating sparse file fails 
86+ 			if  ($ absPathfalse ) {
87+ 				$ absPath$ this getLocalFile ($ file$ size
88+ 			}
7389			if  ($ absPathfalse ) {
7490				Server::get (LoggerInterface::class)->error (
7591					'Failed to get local file to generate thumbnail for:  '  . $ filegetPath (),
@@ -78,11 +94,11 @@ public function getThumbnail(File $file, int $maxX, int $maxY): ?IImage {
7894				return  null ;
7995			}
8096
81- 			$ result  =  $ this -> generateThumbNail ( $ maxX ,  $ maxY ,  $ absPath ,  5 ); 
82- 			if  ($ result  ===  null ) {
83- 				$ result$ this generateThumbNail ($ maxX$ maxY$ absPath1 );
84- 				if  ($ result= == null ) {
85- 					$ result  =  $ this -> generateThumbNail ( $ maxX ,  $ maxY ,  $ absPath ,  0 ) ;
97+ 			// Attempt still image grabs from selected timestamps 
98+ 			foreach  ($ timeAttempts   as   $ timeStamp 
99+ 				$ result$ this generateThumbNail ($ maxX$ maxY$ absPath$ timeStamp 
100+ 				if  ($ result! == null ) {
101+ 					break ;
86102				}
87103			}
88104
@@ -92,10 +108,111 @@ public function getThumbnail(File $file, int $maxX, int $maxY): ?IImage {
92108				break ;
93109			}
94110		}
95- 
96111		return  $ result
97112	}
98113
114+ 	private  function  getSparseFile (File $ fileint  $ sizestring |false  {
115+ 		// File is smaller than $size or file is larger than max int size 
116+ 		// of the host so return false so getLocalFile method is used 
117+ 		if  (($ size$ filegetSize ()) || ($ filegetSize () > PHP_INT_MAX )) {
118+ 			return  false ;
119+ 		}
120+ 		$ content$ filefopen ('r ' );
121+ 
122+ 		// Stream does not support seeking so generating a sparse file is not possible. 
123+ 		if  (stream_get_meta_data ($ content'seekable ' ] !== true ) {
124+ 			fclose ($ content
125+ 			return  false ;
126+ 		}
127+ 
128+ 		$ absPathget (ITempManager::class)->getTemporaryFile ();
129+ 		if  ($ absPathfalse ) {
130+ 			Server::get (LoggerInterface::class)->error (
131+ 				'Failed to get sparse file to generate thumbnail:  '  . $ filegetPath (),
132+ 				['app '  => 'core ' ]
133+ 			);
134+ 			fclose ($ content
135+ 			return  false ;
136+ 		}
137+ 		$ sparseFilefopen ($ absPath'w ' );
138+ 
139+ 		// Firsts 4 bytes indicate length of 1st atom. 
140+ 		$ ftypSizeint )hexdec (bin2hex (stream_get_contents ($ content4 , 0 )));
141+ 		// Download next 4 bytes to find name of 1st atom. 
142+ 		$ ftypLabelstream_get_contents ($ content4 , 4 );
143+ 
144+ 		// MP4/MOVs all begin with the 'ftyp' atom. Anything else is not MP4/MOV 
145+ 		// and therefore should be processed differently. 
146+ 		if  ($ ftypLabel'ftyp ' ) {
147+ 			// Set offset for 2nd atom. Atoms begin where the previous one ends. 
148+ 			$ offset$ ftypSize
149+ 			$ moovSize0 ;
150+ 			$ moovOffset0 ;
151+ 			// Iterate and seek from atom to until the 'moov' atom is found or 
152+ 			// EOF is reached 
153+ 			while  (($ offset8  < $ filegetSize ()) && ($ moovSize0 )) {
154+ 				// First 4 bytes of atom header indicates size of the atom. 
155+ 				$ atomSizeint )hexdec (bin2hex (stream_get_contents ($ content4 , (int )$ offset
156+ 				// Next 4 bytes of atom header is the name/label of the atom 
157+ 				$ atomLabelstream_get_contents ($ content4 , (int )($ offset4 ));
158+ 				// Size value has two special values that don't directly indicate size 
159+ 				// 0 = atom size equals the rest of the file 
160+ 				if  ($ atomSize0 ) {
161+ 					$ atomSize$ filegetsize () - $ offset
162+ 				} else  {
163+ 					// 1 = read an additional 8 bytes after the label to get the 64 bit 
164+ 					// size of the atom. Needed for large atoms like 'mdat' (the video data) 
165+ 					if  ($ atomSize1 ) {
166+ 						$ atomSizeint )hexdec (bin2hex (stream_get_contents ($ content8 , (int )($ offset8 ))));
167+ 					}
168+ 				}
169+ 				// Found the 'moov' atom, store its location and size 
170+ 				if  ($ atomLabel'moov ' ) {
171+ 					$ moovSize$ atomSize
172+ 					$ moovOffset$ offset
173+ 					break ;
174+ 				}
175+ 				$ offset$ atomSize
176+ 			}
177+ 			// 'moov' atom wasn't found or larger than $size 
178+ 			// 'moov' atoms are generally small relative to video length. 
179+ 			// Examples: 
180+ 			// 4K HDR H265 60 FPS, 10 second video = 12.5 KB 'moov' atom, 54 MB total file size 
181+ 			// 4K HDR H265 60 FPS, 5 minute video = 330 KB 'moov' atom, 1.95 GB total file size 
182+ 			// Capping it at $size is a precaution against a corrupt/malicious 'moov' atom. 
183+ 			// This effectively caps the total download size to 2x $size. 
184+ 			// Also, if the 'moov' atom size+offset extends past EOF, it is invalid. 
185+ 			if  (($ moovSize0 ) || ($ moovSize$ size$ moovOffset$ moovSize$ filegetSize ())) {
186+ 				fclose ($ content
187+ 				fclose ($ sparseFile
188+ 				return  false ;
189+ 			}
190+ 			// Generate new file of same size 
191+ 			ftruncate ($ sparseFileint )($ filegetSize ()));
192+ 			fseek ($ sparseFile0 );
193+ 			fseek ($ content0 );
194+ 			// Copy first $size bytes of video into new file 
195+ 			stream_copy_to_stream ($ content$ sparseFile$ size0 );
196+ 
197+ 			// If 'moov' is located before $size in the video, it was already streamed, 
198+ 			// so no need to download it again. 
199+ 			if  ($ moovOffset$ size
200+ 				// Seek to where 'moov' atom needs to be placed 
201+ 				fseek ($ contentint )$ moovOffset
202+ 				fseek ($ sparseFileint )$ moovOffset
203+ 				stream_copy_to_stream ($ content$ sparseFileint )$ moovSize0 );
204+ 			}
205+ 		} else  {
206+ 			// 'ftyp' atom not found, not a valid MP4/MOV 
207+ 			fclose ($ content
208+ 			fclose ($ sparseFile
209+ 			return  false ;
210+ 		}
211+ 		fclose ($ content
212+ 		fclose ($ sparseFile
213+ 		return  $ absPath
214+ 	}
215+ 
99216	private  function  useHdr (string  $ absPathbool  {
100217		// load ffprobe path from configuration, otherwise generate binary path using ffmpeg binary path 
101218		$ ffprobe_binary$ this config ->getSystemValue ('preview_ffprobe_path ' , null ) ?? (pathinfo ($ this binary , PATHINFO_DIRNAME ) . '/ffprobe ' );
@@ -124,7 +241,6 @@ private function useHdr(string $absPath): bool {
124241
125242	private  function  generateThumbNail (int  $ maxXint  $ maxYstring  $ absPathint  $ secondIImage 
126243		$ tmpPathget (ITempManager::class)->getTemporaryFile ();
127- 
128244		if  ($ tmpPathfalse ) {
129245			Server::get (LoggerInterface::class)->error (
130246				'Failed to get local file to generate thumbnail for:  '  . $ absPath
0 commit comments