Complex classes like VideoTranscoder often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.
Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.
While breaking up the class, it is a good idea to analyze how other classes use VideoTranscoder, and based on these observations, apply Extract Interface, too.
1 | <?php |
||
14 | class VideoTranscoder extends BasicTranscoder |
||
15 | { |
||
16 | // Errors |
||
17 | const EXEC_VALIDATE_FAILED = "EXEC_VALIDATE_FAILED"; |
||
18 | const GET_VIDEO_INFO_FAILED = "GET_VIDEO_INFO_FAILED"; |
||
19 | const GET_AUDIO_INFO_FAILED = "GET_AUDIO_INFO_FAILED"; |
||
20 | const GET_DURATION_FAILED = "GET_DURATION_FAILED"; |
||
21 | const NO_OUTPUT = "NO_OUTPUT"; |
||
22 | const BAD_OUTPUT = "BAD_OUTPUT"; |
||
23 | const NO_PRESET = "NO_PRESET"; |
||
24 | const BAD_PRESETS_DIR = "BAD_PRESETS_DIR"; |
||
25 | const UNKNOWN_PRESET = "UNKNOWN_PRESET"; |
||
26 | const OPEN_PRESET_FAILED = "OPEN_PRESET_FAILED"; |
||
27 | const BAD_PRESET_FORMAT = "BAD_PRESET_FORMAT"; |
||
28 | const RATIO_ERROR = "RATIO_ERROR"; |
||
29 | const ENLARGEMENT_ERROR = "ENLARGEMENT_ERROR"; |
||
30 | const TRANSCODE_FAIL = "TRANSCODE_FAIL"; |
||
31 | const WATERMARK_ERROR = "WATERMARK_ERROR"; |
||
32 | |||
33 | const SNAPSHOT_SEC_DEFAULT = 0; |
||
34 | const INTERVALS_DEFAULT = 10; |
||
35 | |||
36 | |||
37 | /*********************** |
||
38 | * TRANSCODE INPUT VIDEO |
||
39 | * Below is the code used to transcode videos based on the JSON format |
||
40 | **********************/ |
||
41 | |||
42 | // $metadata should contain the ffprobe video stream array. |
||
43 | |||
44 | // Start FFmpeg for output transcoding |
||
45 | public function transcode_asset( |
||
46 | $tmpPathInput, |
||
47 | $pathToInputFile, |
||
48 | $pathToOutputFiles, |
||
49 | $metadata = null, |
||
50 | $outputWanted) |
||
51 | { |
||
52 | if (!$metadata) |
||
53 | throw new CpeSdk\CpeException( |
||
54 | "NO Input Video metadata! We can't transcode an asset without probing it first. Use ValidateAsset activity to probe it and pass a 'metadata' field containing the input metadata to this TranscodeAsset activity.", |
||
55 | self::TRANSCODE_FAIL |
||
56 | ); |
||
57 | // Extract an sanitize metadata |
||
58 | $metadata = $this->_extractFileInfo($metadata); |
||
59 | |||
60 | $ffmpegCmd = ""; |
||
61 | |||
62 | // Custom command |
||
63 | if (isset($outputWanted->{'custom_cmd'}) && |
||
64 | $outputWanted->{'custom_cmd'}) { |
||
65 | $ffmpegCmd = $this->craft_ffmpeg_custom_cmd( |
||
66 | $tmpPathInput, |
||
67 | $pathToInputFile, |
||
68 | $pathToOutputFiles, |
||
69 | $metadata, |
||
70 | $outputWanted |
||
71 | ); |
||
72 | } else if ($outputWanted->{'type'} == self::VIDEO) { |
||
73 | $ffmpegCmd = $this->craft_ffmpeg_cmd_video( |
||
74 | $tmpPathInput, |
||
75 | $pathToInputFile, |
||
76 | $pathToOutputFiles, |
||
77 | $metadata, |
||
78 | $outputWanted |
||
79 | ); |
||
80 | } else if ($outputWanted->{'type'} == self::THUMB) { |
||
81 | $ffmpegCmd = $this->craft_ffmpeg_cmd_thumb( |
||
82 | $tmpPathInput, |
||
83 | $pathToInputFile, |
||
84 | $pathToOutputFiles, |
||
85 | $metadata, |
||
86 | $outputWanted |
||
87 | ); |
||
88 | } |
||
89 | |||
90 | $this->cpeLogger->log_out( |
||
91 | "INFO", |
||
92 | basename(__FILE__), |
||
93 | "FFMPEG CMD:\n$ffmpegCmd\n", |
||
94 | $this->activityLogKey |
||
95 | ); |
||
96 | |||
97 | $this->cpeLogger->log_out( |
||
98 | "INFO", |
||
99 | basename(__FILE__), |
||
100 | "Start Transcoding Asset '$pathToInputFile' ...", |
||
101 | $this->activityLogKey |
||
102 | ); |
||
103 | |||
104 | if ($metadata) |
||
|
|||
105 | $this->cpeLogger->log_out( |
||
106 | "INFO", |
||
107 | basename(__FILE__), |
||
108 | "Input Video metadata: " . print_r($metadata, true), |
||
109 | $this->activityLogKey |
||
110 | ); |
||
111 | |||
112 | try { |
||
113 | // Use executer to start FFMpeg command |
||
114 | // Use 'capture_progression' function as callback |
||
115 | // Pass video 'duration' as parameter |
||
116 | // Sleep 1sec between turns and callback every 10 turns |
||
117 | // Output progression logs (true) |
||
118 | $this->executer->execute( |
||
119 | $ffmpegCmd, |
||
120 | 1, |
||
121 | array(2 => array("pipe", "w")), |
||
122 | array($this, "capture_progression"), |
||
123 | $metadata['duration'], |
||
124 | true, |
||
125 | 10 |
||
126 | ); |
||
127 | |||
128 | // Test if we have an output file ! |
||
129 | if (!file_exists($pathToOutputFiles) || |
||
130 | $this->is_dir_empty($pathToOutputFiles)) { |
||
131 | throw new CpeSdk\CpeException( |
||
132 | "Output file '$pathToOutputFiles' hasn't been created successfully or is empty !", |
||
133 | self::TRANSCODE_FAIL |
||
134 | ); |
||
135 | } |
||
136 | |||
137 | // FFProbe the output file and return its information |
||
138 | $output_info = |
||
139 | $this->get_asset_info($pathToOutputFiles."/".$outputWanted->{'output_file_info'}['basename']); |
||
140 | } |
||
141 | catch (\Exception $e) { |
||
142 | $this->cpeLogger->log_out( |
||
143 | "ERROR", |
||
144 | basename(__FILE__), |
||
145 | "Execution of command '".$ffmpegCmd."' failed: " . print_r($metadata, true). ". ".$e->getMessage(), |
||
146 | $this->activityLogKey |
||
147 | ); |
||
148 | return false; |
||
149 | } |
||
150 | |||
151 | // No error. Transcode successful |
||
152 | $this->cpeLogger->log_out( |
||
153 | "INFO", |
||
154 | basename(__FILE__), |
||
155 | "Transcoding successfull !", |
||
156 | $this->activityLogKey |
||
157 | ); |
||
158 | |||
159 | return $output_info; |
||
160 | } |
||
161 | |||
162 | // Craft custom command |
||
163 | private function craft_ffmpeg_custom_cmd( |
||
164 | $tmpPathInput, |
||
165 | $pathToInputFile, |
||
166 | $pathToOutputFiles, |
||
167 | $metadata, |
||
168 | $outputWanted) |
||
169 | { |
||
170 | $ffmpegCmd = $outputWanted->{'custom_cmd'}; |
||
171 | |||
172 | // Replace ${input_file} by input file path |
||
173 | $pathToInputFile = escapeshellarg($pathToInputFile); |
||
174 | $ffmpegCmd = preg_replace('/\$\{input_file\}/', $pathToInputFile, $ffmpegCmd); |
||
175 | |||
176 | $watermarkOptions = ""; |
||
177 | // Process options for watermark |
||
178 | if (isset($outputWanted->{'watermark'}) && $outputWanted->{'watermark'}) { |
||
179 | $watermarkOptions = |
||
180 | $this->get_watermark_options( |
||
181 | $tmpPathInput, |
||
182 | $outputWanted->{'watermark'}); |
||
183 | // Replace ${watermark_options} by watermark options |
||
184 | $ffmpegCmd = preg_replace('/\$\{watermark_options\}/', $watermarkOptions, $ffmpegCmd); |
||
185 | } |
||
186 | |||
187 | // Append output filename to path |
||
188 | $pathToOutputFiles .= "/" . $outputWanted->{'output_file_info'}['basename']; |
||
189 | // Replace ${output_file} by output filename and path to local disk |
||
190 | $ffmpegCmd = preg_replace('/\$\{output_file\}/', $pathToOutputFiles, $ffmpegCmd); |
||
191 | |||
192 | return ($ffmpegCmd); |
||
193 | } |
||
194 | |||
195 | // Generate FFmpeg command for video transcoding |
||
196 | private function craft_ffmpeg_cmd_video( |
||
197 | $tmpPathInput, |
||
198 | $pathToInputFile, |
||
199 | $pathToOutputFiles, |
||
200 | $metadata, |
||
201 | $outputWanted) |
||
202 | { |
||
203 | // Check if a size is provided to override preset size |
||
204 | $size = $this->set_output_video_size($metadata, $outputWanted); |
||
205 | $pathToInputFile = escapeshellarg($pathToInputFile); |
||
206 | |||
207 | $videoCodec = $outputWanted->{'preset_values'}->{'video_codec'}; |
||
208 | if (isset($outputWanted->{'video_codec'})) { |
||
209 | $videoCodec = $outputWanted->{'video_codec'}; |
||
210 | } |
||
211 | |||
212 | $audioCodec = $outputWanted->{'preset_values'}->{'audio_codec'}; |
||
213 | if (isset($outputWanted->{'audio_codec'})) { |
||
214 | $audioCodec = $outputWanted->{'audio_codec'}; |
||
215 | } |
||
216 | |||
217 | $videoBitrate = $outputWanted->{'preset_values'}->{'video_bitrate'}; |
||
218 | if (isset($outputWanted->{'video_bitrate'})) { |
||
219 | $videoBitrate = $outputWanted->{'video_bitrate'}; |
||
220 | } |
||
221 | |||
222 | $audioBitrate = $outputWanted->{'preset_values'}->{'audio_bitrate'}; |
||
223 | if (isset($outputWanted->{'audio_bitrate'})) { |
||
224 | $audioBitrate = $outputWanted->{'audio_bitrate'}; |
||
225 | } |
||
226 | |||
227 | $frameRate = $outputWanted->{'preset_values'}->{'frame_rate'}; |
||
228 | if (isset($outputWanted->{'frame_rate'})) { |
||
229 | $frameRate = $outputWanted->{'frame_rate'}; |
||
230 | } |
||
231 | |||
232 | $formattedOptions = ""; |
||
233 | if (isset($outputWanted->{'preset_values'}->{'video_codec_options'})) { |
||
234 | $formattedOptions = |
||
235 | $this->set_output_video_codec_options($outputWanted->{'preset_values'}->{'video_codec_options'}); |
||
236 | } |
||
237 | |||
238 | $watermarkOptions = ""; |
||
239 | // Process options for watermark |
||
240 | if (isset($outputWanted->{'watermark'}) && $outputWanted->{'watermark'}) { |
||
241 | $watermarkOptions = |
||
242 | $this->get_watermark_options( |
||
243 | $tmpPathInput, |
||
244 | $outputWanted->{'watermark'}); |
||
245 | } |
||
246 | |||
247 | // Create FFMpeg arguments |
||
248 | $ffmpegArgs = " -i $pathToInputFile -y -threads 0"; |
||
249 | $ffmpegArgs .= " -s $size"; |
||
250 | $ffmpegArgs .= " -vcodec $videoCodec"; |
||
251 | $ffmpegArgs .= " -acodec $audioCodec"; |
||
252 | $ffmpegArgs .= " -b:v $videoBitrate"; |
||
253 | $ffmpegArgs .= " -b:a $audioBitrate"; |
||
254 | $ffmpegArgs .= " -r $frameRate"; |
||
255 | $ffmpegArgs .= " $formattedOptions"; |
||
256 | $ffmpegArgs .= " $watermarkOptions"; |
||
257 | |||
258 | // Append output filename to path |
||
259 | $pathToOutputFiles .= "/" . $outputWanted->{'output_file_info'}['basename']; |
||
260 | // Final command |
||
261 | $ffmpegCmd = "ffmpeg $ffmpegArgs $pathToOutputFiles"; |
||
262 | |||
263 | return ($ffmpegCmd); |
||
264 | } |
||
265 | |||
266 | // Craft FFMpeg command to generate thumbnails |
||
267 | private function craft_ffmpeg_cmd_thumb( |
||
268 | $tmpPathInput, |
||
269 | $pathToInputFile, |
||
270 | $pathToOutputFiles, |
||
271 | $metadata, |
||
272 | $outputWanted) |
||
273 | { |
||
274 | // FIXME: Use $metadata to improve the FFMpeg command |
||
275 | // inputAssetInfo contains FFprobe output |
||
276 | |||
277 | $frameOptions = ""; |
||
278 | $outputFileInfo = pathinfo($outputWanted->{'file'}); |
||
279 | $pathToInputFile = escapeshellarg($pathToInputFile); |
||
280 | if ($outputWanted->{'mode'} == 'snapshot') |
||
281 | { |
||
282 | $snapshot_sec = self::SNAPSHOT_SEC_DEFAULT; |
||
283 | if (isset($outputWanted->{'snapshot_sec'}) && |
||
284 | $outputWanted->{'snapshot_sec'} > 0) { |
||
285 | $snapshot_sec = $outputWanted->{'snapshot_sec'}; |
||
286 | } |
||
287 | |||
288 | $time = gmdate("H:i:s", $snapshot_sec) . ".000"; |
||
289 | $pathToOutputFiles .= "/" . $outputFileInfo['basename']; |
||
290 | $frameOptions = " -ss $time -vframes 1"; |
||
291 | } |
||
292 | else if ($outputWanted->{'mode'} == 'intervals') |
||
293 | { |
||
294 | $intervals = self::INTERVALS_DEFAULT; |
||
295 | if (isset($outputWanted->{'intervals'}) && |
||
296 | $outputWanted->{'intervals'} > 0) { |
||
297 | $intervals = $outputWanted->{'intervals'}; |
||
298 | } |
||
299 | |||
300 | $pathToOutputFiles .= "/" . $outputFileInfo['filename'] . "%06d." |
||
301 | . $outputFileInfo['extension']; |
||
302 | $frameOptions = " -vf fps=fps=1/$intervals"; |
||
303 | } |
||
304 | |||
305 | // Create FFMpeg arguments |
||
306 | $ffmpegArgs = " -i $pathToInputFile -y -threads 0"; |
||
307 | $ffmpegArgs .= " -vf scale=" . $outputWanted->{'size'}; |
||
308 | $ffmpegArgs .= " $frameOptions -f image2 -q:v 8"; |
||
309 | |||
310 | // Final command |
||
311 | $ffmpegCmd = "ffmpeg $ffmpegArgs $pathToOutputFiles"; |
||
312 | |||
313 | return ($ffmpegCmd); |
||
314 | } |
||
315 | |||
316 | // Get watermark info to generate overlay options for ffmpeg |
||
317 | private function get_watermark_options( |
||
318 | $tmpPathInput, |
||
319 | $watermarkOptions) |
||
320 | { |
||
321 | // Get info about the video in order to save the watermark in same location |
||
322 | $watermarkFileInfo = pathinfo($watermarkOptions->{'file'}); |
||
323 | $watermarkPath = $tmpPathInput."/".$watermarkFileInfo['basename']; |
||
324 | $newWatermarkPath = $tmpPathInput."/new-".$watermarkFileInfo['basename']; |
||
325 | |||
326 | // Get watermark image from S3 |
||
327 | $s3Output = $this->s3Utils->get_file_from_s3( |
||
328 | $watermarkOptions->{'bucket'}, |
||
329 | $watermarkOptions->{'file'}, |
||
330 | $watermarkPath); |
||
331 | |||
332 | $this->cpeLogger->log_out("INFO", |
||
333 | basename(__FILE__), |
||
334 | $s3Output['msg'], |
||
335 | $this->activityLogKey); |
||
336 | |||
337 | // Transform watermark for opacity |
||
338 | $convertCmd = "convert $watermarkPath -alpha on -channel A -evaluate Multiply " . $watermarkOptions->{'opacity'} . " +channel $newWatermarkPath"; |
||
339 | |||
340 | try { |
||
341 | $out = $this->executer->execute($convertCmd, 1, |
||
342 | array(1 => array("pipe", "w"), 2 => array("pipe", "w")), |
||
343 | false, false, |
||
344 | false, 1); |
||
345 | } |
||
346 | catch (\Exception $e) { |
||
347 | $this->cpeLogger->log_out( |
||
348 | "ERROR", |
||
349 | basename(__FILE__), |
||
350 | "Execution of command '".$convertCmd."' failed", |
||
351 | $this->activityLogKey |
||
352 | ); |
||
353 | return false; |
||
354 | } |
||
355 | |||
356 | // Any error ? |
||
357 | if (isset($out['outErr']) && $out['outErr'] != "" && |
||
358 | (!file_exists($newWatermarkPath) || !filesize($newWatermarkPath))) { |
||
359 | throw new CpeSdk\CpeException( |
||
360 | "Error transforming watermark file '$watermarkPath'!", |
||
361 | self::WATERMARK_ERROR); |
||
362 | } |
||
363 | |||
364 | // Format options for FFMpeg |
||
365 | $size = $watermarkOptions->{'size'}; |
||
366 | $positions = $this->get_watermark_position($watermarkOptions); |
||
367 | $formattedOptions = "-vf \"movie=$newWatermarkPath, scale=$size [wm]; [in][wm] overlay=" . $positions['x'] . ':' . $positions['y'] . " [out]\""; |
||
368 | |||
369 | return ($formattedOptions); |
||
370 | } |
||
371 | |||
372 | // Generate the command line option to position the watermark |
||
373 | private function get_watermark_position($watermarkOptions) |
||
374 | { |
||
375 | $positions = array('x' => 0, 'y' => 0); |
||
376 | |||
377 | if ($watermarkOptions->{'x'} >= 0) { |
||
378 | $positions['x'] = $watermarkOptions->{'x'}; |
||
379 | } |
||
380 | if ($watermarkOptions->{'y'} >= 0) { |
||
381 | $positions['y'] = $watermarkOptions->{'y'}; |
||
382 | } |
||
383 | if ($watermarkOptions->{'x'} < 0) { |
||
384 | $positions['x'] = 'main_w-overlay_w' . $watermarkOptions->{'x'}; |
||
385 | } |
||
386 | if ($watermarkOptions->{'y'} < 0) { |
||
387 | $positions['y'] = 'main_h-overlay_h' . $watermarkOptions->{'y'}; |
||
388 | } |
||
389 | |||
390 | return ($positions); |
||
391 | } |
||
392 | |||
393 | // Get Video codec options and format the options properly for ffmpeg |
||
394 | private function set_output_video_codec_options($videoCodecOptions) |
||
395 | { |
||
396 | $formattedOptions = ""; |
||
397 | $options = explode(",", $videoCodecOptions); |
||
398 | |||
399 | foreach ($options as $option) |
||
400 | { |
||
401 | $keyVal = explode("=", $option); |
||
402 | if ($keyVal[0] === 'Profile') { |
||
403 | $formattedOptions .= " -profile:v ".$keyVal[1]; |
||
404 | } else if ($keyVal[0] === 'Level') { |
||
405 | $formattedOptions .= " -level ".$keyVal[1]; |
||
406 | } else if ($keyVal[0] === 'MaxReferenceFrames') { |
||
407 | $formattedOptions .= " -refs ".$keyVal[1]; |
||
408 | } |
||
409 | } |
||
410 | |||
411 | return ($formattedOptions); |
||
412 | } |
||
413 | |||
414 | // Verify Ratio and Size of output file to ensure it respect restrictions |
||
415 | // Return the output video size |
||
416 | private function set_output_video_size(&$metadata, $outputWanted) |
||
417 | { |
||
418 | // Handle video size |
||
419 | $size = $outputWanted->{'preset_values'}->{'size'}; |
||
420 | if (isset($outputWanted->{'size'})) { |
||
421 | $size = $outputWanted->{'size'}; |
||
422 | } |
||
423 | |||
424 | // Ratio check |
||
425 | if (!isset($outputWanted->{'keep_ratio'}) || |
||
426 | $outputWanted->{'keep_ratio'} == 'true') |
||
427 | { |
||
428 | // FIXME: Improve ratio check |
||
429 | |||
430 | /* $outputRatio = floatval($this->get_ratio($size)); */ |
||
431 | /* $inputRatio = floatval($metadata->{'ratio'}); */ |
||
432 | |||
433 | /* if ($outputRatio != $inputRatio) */ |
||
434 | /* throw new CpeSdk\CpeException( */ |
||
435 | /* "Output video ratio is different from input video: input_ratio: '$inputRatio' / output_ratio: '$outputRatio'. 'keep_ratio' option is enabled (default). Disable it to allow ratio change.", */ |
||
436 | /* self::RATIO_ERROR */ |
||
437 | /* ); */ |
||
438 | } |
||
439 | |||
440 | // Enlargement check |
||
441 | if (!isset($outputWanted->{'allow_upscale'}) || |
||
442 | $outputWanted->{'allow_upscale'} == 'false') |
||
443 | { |
||
444 | $metadata['size'] = $metadata['video']['resolution']; |
||
445 | $inputSize = $metadata['size']; |
||
446 | $inputSizeSplit = explode("x", $inputSize); |
||
447 | $outputSizeSplit = explode("x", $size); |
||
448 | |||
449 | if (intval($outputSizeSplit[0]) > intval($inputSizeSplit[0]) || |
||
450 | intval($outputSizeSplit[1]) > intval($inputSizeSplit[1])) { |
||
451 | $this->cpeLogger->log_out( |
||
452 | "INFO", |
||
453 | basename(__FILE__), |
||
454 | "Requested transcode size is bigger than the original. `allow_upscale` option not provided", |
||
455 | $this->activityLogKey |
||
456 | ); |
||
457 | $size = $metadata['size']; |
||
458 | } |
||
459 | } |
||
460 | |||
461 | return ($size); |
||
462 | } |
||
463 | |||
464 | // REad ffmpeg output and calculate % progress |
||
465 | // This is a callback called from 'CommandExecuter.php' |
||
466 | // $out and $outErr contain FFmpeg output |
||
467 | public function capture_progression($duration, $out, $outErr) |
||
516 | |||
517 | // Combine preset and custom output settings to generate output settings |
||
518 | public function get_preset_values($output_wanted) |
||
545 | |||
546 | // Check if the preset exists |
||
547 | public function validate_preset($output) |
||
594 | |||
595 | // Extract Metadata from ffprobe |
||
596 | private function _extractFileInfo($metadata) { |
||
597 | |||
598 | $videoStreams; |
||
599 | $audioStreams; |
||
600 | |||
601 | foreach ($metadata->streams as $key => $value) { |
||
602 | if ($value->codec_type === 'video') { |
||
632 | |||
633 | /************************************** |
||
634 | * GET VIDEO INFORMATION AND VALIDATION |
||
635 | * The methods below are used by the ValidationActivity |
||
636 | * We capture as much info as possible on the input video |
||
637 | */ |
||
638 | |||
639 | // Execute FFMpeg to get video information |
||
640 | public function get_asset_info($pathToInputFile) |
||
680 | } |
This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.
Consider making the comparison explicit by using
empty(..)
or! empty(...)
instead.