Completed
Push — master ( 7619f7...567a98 )
by
unknown
29:04 queued 10s
created
lib/private/Preview/Movie.php 2 patches
Indentation   +317 added lines, -317 removed lines patch added patch discarded remove patch
@@ -17,321 +17,321 @@
 block discarded – undo
17 17
 use Psr\Log\LoggerInterface;
18 18
 
19 19
 class Movie extends ProviderV2 {
20
-	private IConfig $config;
21
-
22
-	private ?string $binary = null;
23
-
24
-	public function __construct(array $options = []) {
25
-		parent::__construct($options);
26
-		$this->config = Server::get(IConfig::class);
27
-	}
28
-
29
-	public function getMimeType(): string {
30
-		return '/video\/.*/';
31
-	}
32
-
33
-	/**
34
-	 * {@inheritDoc}
35
-	 */
36
-	public function isAvailable(FileInfo $file): bool {
37
-		if (is_null($this->binary)) {
38
-			if (isset($this->options['movieBinary'])) {
39
-				$this->binary = $this->options['movieBinary'];
40
-			}
41
-		}
42
-		return is_string($this->binary);
43
-	}
44
-
45
-	/**
46
-	 * {@inheritDoc}
47
-	 */
48
-	public function getThumbnail(File $file, int $maxX, int $maxY): ?IImage {
49
-		// TODO: use proc_open() and stream the source file ?
50
-
51
-		if (!$this->isAvailable($file)) {
52
-			return null;
53
-		}
54
-
55
-		$result = null;
56
-
57
-		// Timestamps to make attempts to generate a still
58
-		$timeAttempts = [5, 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
-		$sizeAttempts = [1024 * 1024 * 10];
67
-
68
-		if ($this->useTempFile($file)) {
69
-			if ($file->getStorage()->isLocal()) {
70
-				// Temp file required but file is local, so retrieve $sizeAttempt bytes first,
71
-				// and if it doesn't work, retrieve the entire file.
72
-				$sizeAttempts[] = null;
73
-			}
74
-		} else {
75
-			// Temp file is not required and file is local so retrieve entire file.
76
-			$sizeAttempts = [null];
77
-		}
78
-
79
-		foreach ($sizeAttempts as $size) {
80
-			$absPath = false;
81
-			// File is remote, generate a sparse file
82
-			if (!$file->getStorage()->isLocal()) {
83
-				$absPath = $this->getSparseFile($file, $size);
84
-			}
85
-			// Defaults to existing routine if generating sparse file fails
86
-			if ($absPath === false) {
87
-				$absPath = $this->getLocalFile($file, $size);
88
-			}
89
-			if ($absPath === false) {
90
-				Server::get(LoggerInterface::class)->error(
91
-					'Failed to get local file to generate thumbnail for: ' . $file->getPath(),
92
-					['app' => 'core']
93
-				);
94
-				return null;
95
-			}
96
-
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;
102
-				}
103
-				Server::get(LoggerInterface::class)->debug(
104
-					'Movie preview generation attempt failed'
105
-						. ', file=' . $file->getPath()
106
-						. ', time=' . $timeStamp
107
-						. ', size=' . ($size ?? 'entire file'),
108
-					['app' => 'core']
109
-				);
110
-			}
111
-
112
-			$this->cleanTmpFiles();
113
-
114
-			if ($result !== null) {
115
-				Server::get(LoggerInterface::class)->debug(
116
-					'Movie preview generation attempt success'
117
-						. ', file=' . $file->getPath()
118
-						. ', time=' . $timeStamp
119
-						. ', size=' . ($size ?? 'entire file'),
120
-					['app' => 'core']
121
-				);
122
-				break;
123
-			}
124
-
125
-		}
126
-		if ($result === null) {
127
-			Server::get(LoggerInterface::class)->error(
128
-				'Movie preview generation process failed'
129
-					. ', file=' . $file->getPath(),
130
-				['app' => 'core']
131
-			);
132
-		}
133
-		return $result;
134
-	}
135
-
136
-	private function getSparseFile(File $file, int $size): string|false {
137
-		// File is smaller than $size or file is larger than max int size
138
-		// of the host so return false so getLocalFile method is used
139
-		if (($size >= $file->getSize()) || ($file->getSize() > PHP_INT_MAX)) {
140
-			return false;
141
-		}
142
-		$content = $file->fopen('r');
143
-
144
-		// Stream does not support seeking so generating a sparse file is not possible.
145
-		if (stream_get_meta_data($content)['seekable'] !== true) {
146
-			fclose($content);
147
-			return false;
148
-		}
149
-
150
-		$absPath = Server::get(ITempManager::class)->getTemporaryFile();
151
-		if ($absPath === false) {
152
-			Server::get(LoggerInterface::class)->error(
153
-				'Failed to get temp file to create sparse file to generate thumbnail: ' . $file->getPath(),
154
-				['app' => 'core']
155
-			);
156
-			fclose($content);
157
-			return false;
158
-		}
159
-		$sparseFile = fopen($absPath, 'w');
160
-
161
-		// Firsts 4 bytes indicate length of 1st atom.
162
-		$ftypSize = (int)hexdec(bin2hex(stream_get_contents($content, 4, 0)));
163
-		// Download next 4 bytes to find name of 1st atom.
164
-		$ftypLabel = stream_get_contents($content, 4, 4);
165
-
166
-		// MP4/MOVs all begin with the 'ftyp' atom. Anything else is not MP4/MOV
167
-		// and therefore should be processed differently.
168
-		if ($ftypLabel === 'ftyp') {
169
-			// Set offset for 2nd atom. Atoms begin where the previous one ends.
170
-			$offset = $ftypSize;
171
-			$moovSize = 0;
172
-			$moovOffset = 0;
173
-			// Iterate and seek from atom to until the 'moov' atom is found or
174
-			// EOF is reached
175
-			while (($offset + 8 < $file->getSize()) && ($moovSize === 0)) {
176
-				// First 4 bytes of atom header indicates size of the atom.
177
-				$atomSize = (int)hexdec(bin2hex(stream_get_contents($content, 4, (int)$offset)));
178
-				// Next 4 bytes of atom header is the name/label of the atom
179
-				$atomLabel = stream_get_contents($content, 4, (int)($offset + 4));
180
-				// Size value has two special values that don't directly indicate size
181
-				// 0 = atom size equals the rest of the file
182
-				if ($atomSize === 0) {
183
-					$atomSize = $file->getsize() - $offset;
184
-				} else {
185
-					// 1 = read an additional 8 bytes after the label to get the 64 bit
186
-					// size of the atom. Needed for large atoms like 'mdat' (the video data)
187
-					if ($atomSize === 1) {
188
-						$atomSize = (int)hexdec(bin2hex(stream_get_contents($content, 8, (int)($offset + 8))));
189
-						// 0 in the 64 bit field should not occur in a valid file, stop processing
190
-						if ($atomSize === 0) {
191
-							return false;
192
-						}
193
-					}
194
-				}
195
-				// Found the 'moov' atom, store its location and size
196
-				if ($atomLabel === 'moov') {
197
-					$moovSize = $atomSize;
198
-					$moovOffset = $offset;
199
-					break;
200
-				}
201
-				$offset += $atomSize;
202
-			}
203
-			// 'moov' atom wasn't found or larger than $size
204
-			// 'moov' atoms are generally small relative to video length.
205
-			// Examples:
206
-			// 4K HDR H265 60 FPS, 10 second video = 12.5 KB 'moov' atom, 54 MB total file size
207
-			// 4K HDR H265 60 FPS, 5 minute video = 330 KB 'moov' atom, 1.95 GB total file size
208
-			// Capping it at $size is a precaution against a corrupt/malicious 'moov' atom.
209
-			// This effectively caps the total download size to 2x $size.
210
-			// Also, if the 'moov' atom size+offset extends past EOF, it is invalid.
211
-			if (($moovSize === 0) || ($moovSize > $size) || ($moovOffset + $moovSize > $file->getSize())) {
212
-				fclose($content);
213
-				fclose($sparseFile);
214
-				return false;
215
-			}
216
-			// Generate new file of same size
217
-			ftruncate($sparseFile, (int)($file->getSize()));
218
-			fseek($sparseFile, 0);
219
-			fseek($content, 0);
220
-			// Copy first $size bytes of video into new file
221
-			stream_copy_to_stream($content, $sparseFile, $size, 0);
222
-
223
-			// If 'moov' is located entirely before $size in the video, it was already streamed,
224
-			// so no need to download it again.
225
-			if ($moovOffset + $moovSize >= $size) {
226
-				// Seek to where 'moov' atom needs to be placed
227
-				fseek($content, (int)$moovOffset);
228
-				fseek($sparseFile, (int)$moovOffset);
229
-				stream_copy_to_stream($content, $sparseFile, (int)$moovSize, 0);
230
-			}
231
-		} else {
232
-			// 'ftyp' atom not found, not a valid MP4/MOV
233
-			fclose($content);
234
-			fclose($sparseFile);
235
-			return false;
236
-		}
237
-		fclose($content);
238
-		fclose($sparseFile);
239
-		Server::get(LoggerInterface::class)->debug(
240
-			'Sparse file being utilized for preview generation for ' . $file->getPath(),
241
-			['app' => 'core']
242
-		);
243
-		return $absPath;
244
-	}
245
-
246
-	private function useHdr(string $absPath): bool {
247
-		// load ffprobe path from configuration, otherwise generate binary path using ffmpeg binary path
248
-		$ffprobe_binary = $this->config->getSystemValue('preview_ffprobe_path', null) ?? (pathinfo($this->binary, PATHINFO_DIRNAME) . '/ffprobe');
249
-		// run ffprobe on the video file to get value of "color_transfer"
250
-		$test_hdr_cmd = [$ffprobe_binary,'-select_streams', 'v:0',
251
-			'-show_entries', 'stream=color_transfer',
252
-			'-of', 'default=noprint_wrappers=1:nokey=1',
253
-			$absPath];
254
-		$test_hdr_proc = proc_open($test_hdr_cmd, [1 => ['pipe', 'w'], 2 => ['pipe', 'w']], $test_hdr_pipes);
255
-		if ($test_hdr_proc === false) {
256
-			return false;
257
-		}
258
-		$test_hdr_stdout = trim(stream_get_contents($test_hdr_pipes[1]));
259
-		$test_hdr_stderr = trim(stream_get_contents($test_hdr_pipes[2]));
260
-		proc_close($test_hdr_proc);
261
-		// search build options for libzimg (provides zscale filter)
262
-		$ffmpeg_libzimg_installed = strpos($test_hdr_stderr, '--enable-libzimg');
263
-		// Only values of "smpte2084" and "arib-std-b67" indicate an HDR video.
264
-		// Only return true if video is detected as HDR and libzimg is installed.
265
-		if (($test_hdr_stdout === 'smpte2084' || $test_hdr_stdout === 'arib-std-b67') && $ffmpeg_libzimg_installed !== false) {
266
-			return true;
267
-		} else {
268
-			return false;
269
-		}
270
-	}
271
-
272
-	private function generateThumbNail(int $maxX, int $maxY, string $absPath, int $second): ?IImage {
273
-		$tmpPath = Server::get(ITempManager::class)->getTemporaryFile();
274
-		if ($tmpPath === false) {
275
-			Server::get(LoggerInterface::class)->error(
276
-				'Failed to get local file to generate thumbnail for: ' . $absPath,
277
-				['app' => 'core']
278
-			);
279
-			return null;
280
-		}
281
-
282
-		$binaryType = substr(strrchr($this->binary, '/'), 1);
283
-
284
-		if ($binaryType === 'ffmpeg') {
285
-			if ($this->useHdr($absPath)) {
286
-				// Force colorspace to '2020_ncl' because some videos are
287
-				// tagged incorrectly as 'reserved' resulting in fail if not forced.
288
-				$cmd = [$this->binary, '-y', '-ss', (string)$second,
289
-					'-i', $absPath,
290
-					'-f', 'mjpeg', '-vframes', '1',
291
-					'-vf', 'zscale=min=2020_ncl:t=linear:npl=100,format=gbrpf32le,zscale=p=bt709,tonemap=tonemap=hable:desat=0,zscale=t=bt709:m=bt709:r=tv,format=yuv420p',
292
-					$tmpPath];
293
-			} else {
294
-				// always default to generating preview using non-HDR command
295
-				$cmd = [$this->binary, '-y', '-ss', (string)$second,
296
-					'-i', $absPath,
297
-					'-f', 'mjpeg', '-vframes', '1',
298
-					$tmpPath];
299
-			}
300
-		} else {
301
-			// Not supported
302
-			unlink($tmpPath);
303
-			return null;
304
-		}
305
-
306
-		$proc = proc_open($cmd, [1 => ['pipe', 'w'], 2 => ['pipe', 'w']], $pipes);
307
-		$returnCode = -1;
308
-		$output = '';
309
-		if (is_resource($proc)) {
310
-			$stderr = trim(stream_get_contents($pipes[2]));
311
-			$stdout = trim(stream_get_contents($pipes[1]));
312
-			$returnCode = proc_close($proc);
313
-			$output = $stdout . $stderr;
314
-		}
315
-
316
-		Server::get(LoggerInterface::class)->debug(
317
-			'Movie preview generation output'
318
-				. ', file=' . $absPath
319
-				. ', output=',
320
-			['app' => 'core', 'output' => $output]
321
-		);
322
-
323
-		if ($returnCode === 0) {
324
-			$image = new \OCP\Image();
325
-			$image->loadFromFile($tmpPath);
326
-			if ($image->valid()) {
327
-				unlink($tmpPath);
328
-				$image->scaleDownToFit($maxX, $maxY);
329
-				return $image;
330
-			}
331
-		}
332
-
333
-
334
-		unlink($tmpPath);
335
-		return null;
336
-	}
20
+    private IConfig $config;
21
+
22
+    private ?string $binary = null;
23
+
24
+    public function __construct(array $options = []) {
25
+        parent::__construct($options);
26
+        $this->config = Server::get(IConfig::class);
27
+    }
28
+
29
+    public function getMimeType(): string {
30
+        return '/video\/.*/';
31
+    }
32
+
33
+    /**
34
+     * {@inheritDoc}
35
+     */
36
+    public function isAvailable(FileInfo $file): bool {
37
+        if (is_null($this->binary)) {
38
+            if (isset($this->options['movieBinary'])) {
39
+                $this->binary = $this->options['movieBinary'];
40
+            }
41
+        }
42
+        return is_string($this->binary);
43
+    }
44
+
45
+    /**
46
+     * {@inheritDoc}
47
+     */
48
+    public function getThumbnail(File $file, int $maxX, int $maxY): ?IImage {
49
+        // TODO: use proc_open() and stream the source file ?
50
+
51
+        if (!$this->isAvailable($file)) {
52
+            return null;
53
+        }
54
+
55
+        $result = null;
56
+
57
+        // Timestamps to make attempts to generate a still
58
+        $timeAttempts = [5, 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
+        $sizeAttempts = [1024 * 1024 * 10];
67
+
68
+        if ($this->useTempFile($file)) {
69
+            if ($file->getStorage()->isLocal()) {
70
+                // Temp file required but file is local, so retrieve $sizeAttempt bytes first,
71
+                // and if it doesn't work, retrieve the entire file.
72
+                $sizeAttempts[] = null;
73
+            }
74
+        } else {
75
+            // Temp file is not required and file is local so retrieve entire file.
76
+            $sizeAttempts = [null];
77
+        }
78
+
79
+        foreach ($sizeAttempts as $size) {
80
+            $absPath = false;
81
+            // File is remote, generate a sparse file
82
+            if (!$file->getStorage()->isLocal()) {
83
+                $absPath = $this->getSparseFile($file, $size);
84
+            }
85
+            // Defaults to existing routine if generating sparse file fails
86
+            if ($absPath === false) {
87
+                $absPath = $this->getLocalFile($file, $size);
88
+            }
89
+            if ($absPath === false) {
90
+                Server::get(LoggerInterface::class)->error(
91
+                    'Failed to get local file to generate thumbnail for: ' . $file->getPath(),
92
+                    ['app' => 'core']
93
+                );
94
+                return null;
95
+            }
96
+
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;
102
+                }
103
+                Server::get(LoggerInterface::class)->debug(
104
+                    'Movie preview generation attempt failed'
105
+                        . ', file=' . $file->getPath()
106
+                        . ', time=' . $timeStamp
107
+                        . ', size=' . ($size ?? 'entire file'),
108
+                    ['app' => 'core']
109
+                );
110
+            }
111
+
112
+            $this->cleanTmpFiles();
113
+
114
+            if ($result !== null) {
115
+                Server::get(LoggerInterface::class)->debug(
116
+                    'Movie preview generation attempt success'
117
+                        . ', file=' . $file->getPath()
118
+                        . ', time=' . $timeStamp
119
+                        . ', size=' . ($size ?? 'entire file'),
120
+                    ['app' => 'core']
121
+                );
122
+                break;
123
+            }
124
+
125
+        }
126
+        if ($result === null) {
127
+            Server::get(LoggerInterface::class)->error(
128
+                'Movie preview generation process failed'
129
+                    . ', file=' . $file->getPath(),
130
+                ['app' => 'core']
131
+            );
132
+        }
133
+        return $result;
134
+    }
135
+
136
+    private function getSparseFile(File $file, int $size): string|false {
137
+        // File is smaller than $size or file is larger than max int size
138
+        // of the host so return false so getLocalFile method is used
139
+        if (($size >= $file->getSize()) || ($file->getSize() > PHP_INT_MAX)) {
140
+            return false;
141
+        }
142
+        $content = $file->fopen('r');
143
+
144
+        // Stream does not support seeking so generating a sparse file is not possible.
145
+        if (stream_get_meta_data($content)['seekable'] !== true) {
146
+            fclose($content);
147
+            return false;
148
+        }
149
+
150
+        $absPath = Server::get(ITempManager::class)->getTemporaryFile();
151
+        if ($absPath === false) {
152
+            Server::get(LoggerInterface::class)->error(
153
+                'Failed to get temp file to create sparse file to generate thumbnail: ' . $file->getPath(),
154
+                ['app' => 'core']
155
+            );
156
+            fclose($content);
157
+            return false;
158
+        }
159
+        $sparseFile = fopen($absPath, 'w');
160
+
161
+        // Firsts 4 bytes indicate length of 1st atom.
162
+        $ftypSize = (int)hexdec(bin2hex(stream_get_contents($content, 4, 0)));
163
+        // Download next 4 bytes to find name of 1st atom.
164
+        $ftypLabel = stream_get_contents($content, 4, 4);
165
+
166
+        // MP4/MOVs all begin with the 'ftyp' atom. Anything else is not MP4/MOV
167
+        // and therefore should be processed differently.
168
+        if ($ftypLabel === 'ftyp') {
169
+            // Set offset for 2nd atom. Atoms begin where the previous one ends.
170
+            $offset = $ftypSize;
171
+            $moovSize = 0;
172
+            $moovOffset = 0;
173
+            // Iterate and seek from atom to until the 'moov' atom is found or
174
+            // EOF is reached
175
+            while (($offset + 8 < $file->getSize()) && ($moovSize === 0)) {
176
+                // First 4 bytes of atom header indicates size of the atom.
177
+                $atomSize = (int)hexdec(bin2hex(stream_get_contents($content, 4, (int)$offset)));
178
+                // Next 4 bytes of atom header is the name/label of the atom
179
+                $atomLabel = stream_get_contents($content, 4, (int)($offset + 4));
180
+                // Size value has two special values that don't directly indicate size
181
+                // 0 = atom size equals the rest of the file
182
+                if ($atomSize === 0) {
183
+                    $atomSize = $file->getsize() - $offset;
184
+                } else {
185
+                    // 1 = read an additional 8 bytes after the label to get the 64 bit
186
+                    // size of the atom. Needed for large atoms like 'mdat' (the video data)
187
+                    if ($atomSize === 1) {
188
+                        $atomSize = (int)hexdec(bin2hex(stream_get_contents($content, 8, (int)($offset + 8))));
189
+                        // 0 in the 64 bit field should not occur in a valid file, stop processing
190
+                        if ($atomSize === 0) {
191
+                            return false;
192
+                        }
193
+                    }
194
+                }
195
+                // Found the 'moov' atom, store its location and size
196
+                if ($atomLabel === 'moov') {
197
+                    $moovSize = $atomSize;
198
+                    $moovOffset = $offset;
199
+                    break;
200
+                }
201
+                $offset += $atomSize;
202
+            }
203
+            // 'moov' atom wasn't found or larger than $size
204
+            // 'moov' atoms are generally small relative to video length.
205
+            // Examples:
206
+            // 4K HDR H265 60 FPS, 10 second video = 12.5 KB 'moov' atom, 54 MB total file size
207
+            // 4K HDR H265 60 FPS, 5 minute video = 330 KB 'moov' atom, 1.95 GB total file size
208
+            // Capping it at $size is a precaution against a corrupt/malicious 'moov' atom.
209
+            // This effectively caps the total download size to 2x $size.
210
+            // Also, if the 'moov' atom size+offset extends past EOF, it is invalid.
211
+            if (($moovSize === 0) || ($moovSize > $size) || ($moovOffset + $moovSize > $file->getSize())) {
212
+                fclose($content);
213
+                fclose($sparseFile);
214
+                return false;
215
+            }
216
+            // Generate new file of same size
217
+            ftruncate($sparseFile, (int)($file->getSize()));
218
+            fseek($sparseFile, 0);
219
+            fseek($content, 0);
220
+            // Copy first $size bytes of video into new file
221
+            stream_copy_to_stream($content, $sparseFile, $size, 0);
222
+
223
+            // If 'moov' is located entirely before $size in the video, it was already streamed,
224
+            // so no need to download it again.
225
+            if ($moovOffset + $moovSize >= $size) {
226
+                // Seek to where 'moov' atom needs to be placed
227
+                fseek($content, (int)$moovOffset);
228
+                fseek($sparseFile, (int)$moovOffset);
229
+                stream_copy_to_stream($content, $sparseFile, (int)$moovSize, 0);
230
+            }
231
+        } else {
232
+            // 'ftyp' atom not found, not a valid MP4/MOV
233
+            fclose($content);
234
+            fclose($sparseFile);
235
+            return false;
236
+        }
237
+        fclose($content);
238
+        fclose($sparseFile);
239
+        Server::get(LoggerInterface::class)->debug(
240
+            'Sparse file being utilized for preview generation for ' . $file->getPath(),
241
+            ['app' => 'core']
242
+        );
243
+        return $absPath;
244
+    }
245
+
246
+    private function useHdr(string $absPath): bool {
247
+        // load ffprobe path from configuration, otherwise generate binary path using ffmpeg binary path
248
+        $ffprobe_binary = $this->config->getSystemValue('preview_ffprobe_path', null) ?? (pathinfo($this->binary, PATHINFO_DIRNAME) . '/ffprobe');
249
+        // run ffprobe on the video file to get value of "color_transfer"
250
+        $test_hdr_cmd = [$ffprobe_binary,'-select_streams', 'v:0',
251
+            '-show_entries', 'stream=color_transfer',
252
+            '-of', 'default=noprint_wrappers=1:nokey=1',
253
+            $absPath];
254
+        $test_hdr_proc = proc_open($test_hdr_cmd, [1 => ['pipe', 'w'], 2 => ['pipe', 'w']], $test_hdr_pipes);
255
+        if ($test_hdr_proc === false) {
256
+            return false;
257
+        }
258
+        $test_hdr_stdout = trim(stream_get_contents($test_hdr_pipes[1]));
259
+        $test_hdr_stderr = trim(stream_get_contents($test_hdr_pipes[2]));
260
+        proc_close($test_hdr_proc);
261
+        // search build options for libzimg (provides zscale filter)
262
+        $ffmpeg_libzimg_installed = strpos($test_hdr_stderr, '--enable-libzimg');
263
+        // Only values of "smpte2084" and "arib-std-b67" indicate an HDR video.
264
+        // Only return true if video is detected as HDR and libzimg is installed.
265
+        if (($test_hdr_stdout === 'smpte2084' || $test_hdr_stdout === 'arib-std-b67') && $ffmpeg_libzimg_installed !== false) {
266
+            return true;
267
+        } else {
268
+            return false;
269
+        }
270
+    }
271
+
272
+    private function generateThumbNail(int $maxX, int $maxY, string $absPath, int $second): ?IImage {
273
+        $tmpPath = Server::get(ITempManager::class)->getTemporaryFile();
274
+        if ($tmpPath === false) {
275
+            Server::get(LoggerInterface::class)->error(
276
+                'Failed to get local file to generate thumbnail for: ' . $absPath,
277
+                ['app' => 'core']
278
+            );
279
+            return null;
280
+        }
281
+
282
+        $binaryType = substr(strrchr($this->binary, '/'), 1);
283
+
284
+        if ($binaryType === 'ffmpeg') {
285
+            if ($this->useHdr($absPath)) {
286
+                // Force colorspace to '2020_ncl' because some videos are
287
+                // tagged incorrectly as 'reserved' resulting in fail if not forced.
288
+                $cmd = [$this->binary, '-y', '-ss', (string)$second,
289
+                    '-i', $absPath,
290
+                    '-f', 'mjpeg', '-vframes', '1',
291
+                    '-vf', 'zscale=min=2020_ncl:t=linear:npl=100,format=gbrpf32le,zscale=p=bt709,tonemap=tonemap=hable:desat=0,zscale=t=bt709:m=bt709:r=tv,format=yuv420p',
292
+                    $tmpPath];
293
+            } else {
294
+                // always default to generating preview using non-HDR command
295
+                $cmd = [$this->binary, '-y', '-ss', (string)$second,
296
+                    '-i', $absPath,
297
+                    '-f', 'mjpeg', '-vframes', '1',
298
+                    $tmpPath];
299
+            }
300
+        } else {
301
+            // Not supported
302
+            unlink($tmpPath);
303
+            return null;
304
+        }
305
+
306
+        $proc = proc_open($cmd, [1 => ['pipe', 'w'], 2 => ['pipe', 'w']], $pipes);
307
+        $returnCode = -1;
308
+        $output = '';
309
+        if (is_resource($proc)) {
310
+            $stderr = trim(stream_get_contents($pipes[2]));
311
+            $stdout = trim(stream_get_contents($pipes[1]));
312
+            $returnCode = proc_close($proc);
313
+            $output = $stdout . $stderr;
314
+        }
315
+
316
+        Server::get(LoggerInterface::class)->debug(
317
+            'Movie preview generation output'
318
+                . ', file=' . $absPath
319
+                . ', output=',
320
+            ['app' => 'core', 'output' => $output]
321
+        );
322
+
323
+        if ($returnCode === 0) {
324
+            $image = new \OCP\Image();
325
+            $image->loadFromFile($tmpPath);
326
+            if ($image->valid()) {
327
+                unlink($tmpPath);
328
+                $image->scaleDownToFit($maxX, $maxY);
329
+                return $image;
330
+            }
331
+        }
332
+
333
+
334
+        unlink($tmpPath);
335
+        return null;
336
+    }
337 337
 }
Please login to merge, or discard this patch.
Spacing   +26 added lines, -26 removed lines patch added patch discarded remove patch
@@ -88,7 +88,7 @@  discard block
 block discarded – undo
88 88
 			}
89 89
 			if ($absPath === false) {
90 90
 				Server::get(LoggerInterface::class)->error(
91
-					'Failed to get local file to generate thumbnail for: ' . $file->getPath(),
91
+					'Failed to get local file to generate thumbnail for: '.$file->getPath(),
92 92
 					['app' => 'core']
93 93
 				);
94 94
 				return null;
@@ -102,9 +102,9 @@  discard block
 block discarded – undo
102 102
 				}
103 103
 				Server::get(LoggerInterface::class)->debug(
104 104
 					'Movie preview generation attempt failed'
105
-						. ', file=' . $file->getPath()
106
-						. ', time=' . $timeStamp
107
-						. ', size=' . ($size ?? 'entire file'),
105
+						. ', file='.$file->getPath()
106
+						. ', time='.$timeStamp
107
+						. ', size='.($size ?? 'entire file'),
108 108
 					['app' => 'core']
109 109
 				);
110 110
 			}
@@ -114,9 +114,9 @@  discard block
 block discarded – undo
114 114
 			if ($result !== null) {
115 115
 				Server::get(LoggerInterface::class)->debug(
116 116
 					'Movie preview generation attempt success'
117
-						. ', file=' . $file->getPath()
118
-						. ', time=' . $timeStamp
119
-						. ', size=' . ($size ?? 'entire file'),
117
+						. ', file='.$file->getPath()
118
+						. ', time='.$timeStamp
119
+						. ', size='.($size ?? 'entire file'),
120 120
 					['app' => 'core']
121 121
 				);
122 122
 				break;
@@ -126,14 +126,14 @@  discard block
 block discarded – undo
126 126
 		if ($result === null) {
127 127
 			Server::get(LoggerInterface::class)->error(
128 128
 				'Movie preview generation process failed'
129
-					. ', file=' . $file->getPath(),
129
+					. ', file='.$file->getPath(),
130 130
 				['app' => 'core']
131 131
 			);
132 132
 		}
133 133
 		return $result;
134 134
 	}
135 135
 
136
-	private function getSparseFile(File $file, int $size): string|false {
136
+	private function getSparseFile(File $file, int $size): string | false {
137 137
 		// File is smaller than $size or file is larger than max int size
138 138
 		// of the host so return false so getLocalFile method is used
139 139
 		if (($size >= $file->getSize()) || ($file->getSize() > PHP_INT_MAX)) {
@@ -150,7 +150,7 @@  discard block
 block discarded – undo
150 150
 		$absPath = Server::get(ITempManager::class)->getTemporaryFile();
151 151
 		if ($absPath === false) {
152 152
 			Server::get(LoggerInterface::class)->error(
153
-				'Failed to get temp file to create sparse file to generate thumbnail: ' . $file->getPath(),
153
+				'Failed to get temp file to create sparse file to generate thumbnail: '.$file->getPath(),
154 154
 				['app' => 'core']
155 155
 			);
156 156
 			fclose($content);
@@ -159,7 +159,7 @@  discard block
 block discarded – undo
159 159
 		$sparseFile = fopen($absPath, 'w');
160 160
 
161 161
 		// Firsts 4 bytes indicate length of 1st atom.
162
-		$ftypSize = (int)hexdec(bin2hex(stream_get_contents($content, 4, 0)));
162
+		$ftypSize = (int) hexdec(bin2hex(stream_get_contents($content, 4, 0)));
163 163
 		// Download next 4 bytes to find name of 1st atom.
164 164
 		$ftypLabel = stream_get_contents($content, 4, 4);
165 165
 
@@ -174,9 +174,9 @@  discard block
 block discarded – undo
174 174
 			// EOF is reached
175 175
 			while (($offset + 8 < $file->getSize()) && ($moovSize === 0)) {
176 176
 				// First 4 bytes of atom header indicates size of the atom.
177
-				$atomSize = (int)hexdec(bin2hex(stream_get_contents($content, 4, (int)$offset)));
177
+				$atomSize = (int) hexdec(bin2hex(stream_get_contents($content, 4, (int) $offset)));
178 178
 				// Next 4 bytes of atom header is the name/label of the atom
179
-				$atomLabel = stream_get_contents($content, 4, (int)($offset + 4));
179
+				$atomLabel = stream_get_contents($content, 4, (int) ($offset + 4));
180 180
 				// Size value has two special values that don't directly indicate size
181 181
 				// 0 = atom size equals the rest of the file
182 182
 				if ($atomSize === 0) {
@@ -185,7 +185,7 @@  discard block
 block discarded – undo
185 185
 					// 1 = read an additional 8 bytes after the label to get the 64 bit
186 186
 					// size of the atom. Needed for large atoms like 'mdat' (the video data)
187 187
 					if ($atomSize === 1) {
188
-						$atomSize = (int)hexdec(bin2hex(stream_get_contents($content, 8, (int)($offset + 8))));
188
+						$atomSize = (int) hexdec(bin2hex(stream_get_contents($content, 8, (int) ($offset + 8))));
189 189
 						// 0 in the 64 bit field should not occur in a valid file, stop processing
190 190
 						if ($atomSize === 0) {
191 191
 							return false;
@@ -214,7 +214,7 @@  discard block
 block discarded – undo
214 214
 				return false;
215 215
 			}
216 216
 			// Generate new file of same size
217
-			ftruncate($sparseFile, (int)($file->getSize()));
217
+			ftruncate($sparseFile, (int) ($file->getSize()));
218 218
 			fseek($sparseFile, 0);
219 219
 			fseek($content, 0);
220 220
 			// Copy first $size bytes of video into new file
@@ -224,9 +224,9 @@  discard block
 block discarded – undo
224 224
 			// so no need to download it again.
225 225
 			if ($moovOffset + $moovSize >= $size) {
226 226
 				// Seek to where 'moov' atom needs to be placed
227
-				fseek($content, (int)$moovOffset);
228
-				fseek($sparseFile, (int)$moovOffset);
229
-				stream_copy_to_stream($content, $sparseFile, (int)$moovSize, 0);
227
+				fseek($content, (int) $moovOffset);
228
+				fseek($sparseFile, (int) $moovOffset);
229
+				stream_copy_to_stream($content, $sparseFile, (int) $moovSize, 0);
230 230
 			}
231 231
 		} else {
232 232
 			// 'ftyp' atom not found, not a valid MP4/MOV
@@ -237,7 +237,7 @@  discard block
 block discarded – undo
237 237
 		fclose($content);
238 238
 		fclose($sparseFile);
239 239
 		Server::get(LoggerInterface::class)->debug(
240
-			'Sparse file being utilized for preview generation for ' . $file->getPath(),
240
+			'Sparse file being utilized for preview generation for '.$file->getPath(),
241 241
 			['app' => 'core']
242 242
 		);
243 243
 		return $absPath;
@@ -245,9 +245,9 @@  discard block
 block discarded – undo
245 245
 
246 246
 	private function useHdr(string $absPath): bool {
247 247
 		// load ffprobe path from configuration, otherwise generate binary path using ffmpeg binary path
248
-		$ffprobe_binary = $this->config->getSystemValue('preview_ffprobe_path', null) ?? (pathinfo($this->binary, PATHINFO_DIRNAME) . '/ffprobe');
248
+		$ffprobe_binary = $this->config->getSystemValue('preview_ffprobe_path', null) ?? (pathinfo($this->binary, PATHINFO_DIRNAME).'/ffprobe');
249 249
 		// run ffprobe on the video file to get value of "color_transfer"
250
-		$test_hdr_cmd = [$ffprobe_binary,'-select_streams', 'v:0',
250
+		$test_hdr_cmd = [$ffprobe_binary, '-select_streams', 'v:0',
251 251
 			'-show_entries', 'stream=color_transfer',
252 252
 			'-of', 'default=noprint_wrappers=1:nokey=1',
253 253
 			$absPath];
@@ -273,7 +273,7 @@  discard block
 block discarded – undo
273 273
 		$tmpPath = Server::get(ITempManager::class)->getTemporaryFile();
274 274
 		if ($tmpPath === false) {
275 275
 			Server::get(LoggerInterface::class)->error(
276
-				'Failed to get local file to generate thumbnail for: ' . $absPath,
276
+				'Failed to get local file to generate thumbnail for: '.$absPath,
277 277
 				['app' => 'core']
278 278
 			);
279 279
 			return null;
@@ -285,14 +285,14 @@  discard block
 block discarded – undo
285 285
 			if ($this->useHdr($absPath)) {
286 286
 				// Force colorspace to '2020_ncl' because some videos are
287 287
 				// tagged incorrectly as 'reserved' resulting in fail if not forced.
288
-				$cmd = [$this->binary, '-y', '-ss', (string)$second,
288
+				$cmd = [$this->binary, '-y', '-ss', (string) $second,
289 289
 					'-i', $absPath,
290 290
 					'-f', 'mjpeg', '-vframes', '1',
291 291
 					'-vf', 'zscale=min=2020_ncl:t=linear:npl=100,format=gbrpf32le,zscale=p=bt709,tonemap=tonemap=hable:desat=0,zscale=t=bt709:m=bt709:r=tv,format=yuv420p',
292 292
 					$tmpPath];
293 293
 			} else {
294 294
 				// always default to generating preview using non-HDR command
295
-				$cmd = [$this->binary, '-y', '-ss', (string)$second,
295
+				$cmd = [$this->binary, '-y', '-ss', (string) $second,
296 296
 					'-i', $absPath,
297 297
 					'-f', 'mjpeg', '-vframes', '1',
298 298
 					$tmpPath];
@@ -310,12 +310,12 @@  discard block
 block discarded – undo
310 310
 			$stderr = trim(stream_get_contents($pipes[2]));
311 311
 			$stdout = trim(stream_get_contents($pipes[1]));
312 312
 			$returnCode = proc_close($proc);
313
-			$output = $stdout . $stderr;
313
+			$output = $stdout.$stderr;
314 314
 		}
315 315
 
316 316
 		Server::get(LoggerInterface::class)->debug(
317 317
 			'Movie preview generation output'
318
-				. ', file=' . $absPath
318
+				. ', file='.$absPath
319 319
 				. ', output=',
320 320
 			['app' => 'core', 'output' => $output]
321 321
 		);
Please login to merge, or discard this patch.