1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
declare(strict_types=1); |
4
|
|
|
|
5
|
|
|
namespace Soluble\MediaTools; |
6
|
|
|
|
7
|
|
|
use Soluble\MediaTools\Config\FFMpegConfig; |
8
|
|
|
use Soluble\MediaTools\Exception\FileNotFoundException; |
9
|
|
|
use Soluble\MediaTools\Exception\ProcessConversionException; |
10
|
|
|
use Soluble\MediaTools\Util\Assert\PathAssertionsTrait; |
11
|
|
|
use Soluble\MediaTools\Video\Converter\ParamsInterface; |
12
|
|
|
use Soluble\MediaTools\Video\Filter\EmptyVideoFilter; |
13
|
|
|
use Soluble\MediaTools\Video\Filter\VideoFilterChain; |
14
|
|
|
use Soluble\MediaTools\Video\Filter\VideoFilterInterface; |
15
|
|
|
use Soluble\MediaTools\Video\Filter\VideoFilterTypeDenoiseInterface; |
16
|
|
|
use Soluble\MediaTools\Video\Filter\YadifVideoFilter; |
17
|
|
|
use Soluble\MediaTools\Video\VideoConverterServiceInterface; |
18
|
|
|
use Symfony\Component\Process\Exception as ProcessException; |
19
|
|
|
use Symfony\Component\Process\Process; |
20
|
|
|
|
21
|
|
|
class VideoConverter implements VideoConverterServiceInterface |
22
|
|
|
{ |
23
|
|
|
use PathAssertionsTrait; |
24
|
|
|
|
25
|
|
|
/** @var FFMpegConfig */ |
26
|
|
|
protected $ffmpegConfig; |
27
|
|
|
|
28
|
|
|
/** @var VideoProbe */ |
29
|
|
|
protected $videoProbe; |
30
|
|
|
|
31
|
5 |
|
public function __construct(FFMpegConfig $ffmpegConfig, VideoProbe $videoProbe) |
32
|
|
|
{ |
33
|
5 |
|
$this->videoProbe = $videoProbe; |
34
|
5 |
|
$this->ffmpegConfig = $ffmpegConfig; |
35
|
5 |
|
$this->ffmpegConfig->getProcess()->ensureBinaryExists(); |
36
|
5 |
|
} |
37
|
|
|
|
38
|
|
|
/** |
39
|
|
|
* Return ready-to-run symfony process object that you can use |
40
|
|
|
* to `run()` or `start()` programmatically. Useful if you want to make |
41
|
|
|
* things async... |
42
|
|
|
* |
43
|
|
|
* @see https://symfony.com/doc/current/components/process.html |
44
|
|
|
* |
45
|
|
|
* @throws FileNotFoundException when inputFile does not exists |
46
|
|
|
*/ |
47
|
5 |
|
public function getConversionProcess(string $inputFile, string $outputFile, VideoConvertParams $convertParams): Process |
48
|
|
|
{ |
49
|
5 |
|
$this->ensureFileExists($inputFile); |
50
|
|
|
|
51
|
4 |
|
$process = $this->ffmpegConfig->getProcess(); |
52
|
|
|
|
53
|
4 |
|
if (!$convertParams->hasOption(ParamsInterface::PARAM_THREADS) && $this->ffmpegConfig->getThreads() !== null) { |
54
|
|
|
$convertParams = $convertParams->withThreads($this->ffmpegConfig->getThreads()); |
55
|
|
|
} |
56
|
|
|
|
57
|
4 |
|
$ffmpegCmd = $process->buildCommand( |
58
|
4 |
|
array_merge( |
59
|
|
|
[ |
60
|
4 |
|
sprintf('-i %s', escapeshellarg($inputFile)), // input filename |
61
|
|
|
], |
62
|
4 |
|
$convertParams->getFFMpegArguments(), |
63
|
|
|
[ |
64
|
4 |
|
'-y', // tell to overwrite |
65
|
4 |
|
sprintf('%s', escapeshellarg($outputFile)), |
66
|
|
|
] |
67
|
|
|
) |
68
|
|
|
); |
69
|
|
|
|
70
|
4 |
|
$process = new Process($ffmpegCmd); |
71
|
4 |
|
$process->setTimeout($this->ffmpegConfig->getConversionTimeout()); |
72
|
4 |
|
$process->setIdleTimeout($this->ffmpegConfig->getConversionIdleTimeout()); |
73
|
|
|
|
74
|
4 |
|
return $process; |
75
|
|
|
} |
76
|
|
|
|
77
|
|
|
/** |
78
|
|
|
* Run a conversion, throw exception on error. |
79
|
|
|
* |
80
|
|
|
* @param callable|null $callback A PHP callback to run whenever there is some |
81
|
|
|
* output available on STDOUT or STDERR |
82
|
|
|
* @param array<string,string|int>|null $env An array of env vars to set |
83
|
|
|
* when running the process |
84
|
|
|
* |
85
|
|
|
* @throws FileNotFoundException When inputFile does not exists |
86
|
|
|
* @throws ProcessConversionException When the ffmpeg process conversion failed |
87
|
|
|
*/ |
88
|
5 |
|
public function convert(string $inputFile, string $outputFile, VideoConvertParams $convertParams, ?callable $callback = null, ?array $env = null): void |
89
|
|
|
{ |
90
|
5 |
|
$process = $this->getConversionProcess($inputFile, $outputFile, $convertParams); |
91
|
|
|
|
92
|
|
|
try { |
93
|
4 |
|
$process->mustRun($callback, (is_array($env) ? $env : $this->ffmpegConfig->getConversionEnv())); |
94
|
2 |
|
} catch (ProcessException\RuntimeException $symfonyProcessException) { |
95
|
|
|
// will include: ProcessFailedException|ProcessTimedOutException|ProcessSignaledException |
|
|
|
|
96
|
2 |
|
throw new ProcessConversionException($process, $symfonyProcessException); |
97
|
|
|
} catch (FileNotFoundException $e) { |
98
|
|
|
throw $e; |
99
|
|
|
} |
100
|
2 |
|
} |
101
|
|
|
|
102
|
|
|
/** |
103
|
|
|
* Try to guess if the original video is interlaced (bff, tff) and |
104
|
|
|
* return ffmpeg yadif filter argument and add denoise filter if any. |
105
|
|
|
* |
106
|
|
|
* @see https://ffmpeg.org/ffmpeg-filters.html (section yadif) |
107
|
|
|
* @see https://askubuntu.com/a/867203 |
108
|
|
|
* |
109
|
|
|
* @return VideoFilterInterface|VideoFilterChain|EmptyVideoFilter|YadifVideoFilter |
110
|
|
|
*/ |
111
|
|
|
public function getDeintFilter(string $videoFile, ?VideoFilterTypeDenoiseInterface $denoiseFilter = null): VideoFilterInterface |
112
|
|
|
{ |
113
|
|
|
$guess = $this->videoProbe->guessInterlacing($videoFile); |
114
|
|
|
$deintFilter = $guess->getDeinterlaceVideoFilter(); |
115
|
|
|
// skip all filters if video is not interlaces |
116
|
|
|
if ($deintFilter instanceof EmptyVideoFilter) { |
117
|
|
|
return $deintFilter; |
118
|
|
|
} |
119
|
|
|
if ($denoiseFilter !== null) { |
120
|
|
|
$videoFilterChain = new VideoFilterChain(); |
121
|
|
|
$videoFilterChain->addFilter($deintFilter); |
122
|
|
|
$videoFilterChain->addFilter($denoiseFilter); |
123
|
|
|
|
124
|
|
|
return $videoFilterChain; |
125
|
|
|
} |
126
|
|
|
|
127
|
|
|
return $deintFilter; |
128
|
|
|
} |
129
|
|
|
|
130
|
|
|
/* |
|
|
|
|
131
|
|
|
public function transcodeMultiPass(string $videoFile, string $outputFile, VideoConvertParams $convertParams, VideoFilterInterface $videoFilter=null): void { |
132
|
|
|
|
133
|
|
|
$this->ensureFileExists($videoFile); |
134
|
|
|
if ($videoFilter === null) { |
135
|
|
|
$videoFilter = new EmptyVideoFilter(); |
136
|
|
|
} |
137
|
|
|
|
138
|
|
|
|
139
|
|
|
$threads = $convertParams->getOption(VideoConvertParams::OPTION_THREADS, $this->ffmpegConfig->getThreads()); |
140
|
|
|
|
141
|
|
|
$ffmpegBin = $this->ffmpegConfig->getBinary(); |
142
|
|
|
|
143
|
|
|
$commonArgs = array_merge([ |
144
|
|
|
$ffmpegBin, |
145
|
|
|
sprintf('-i %s', escapeshellarg($videoFile)), // input filename |
146
|
|
|
$videoFilter->getFFMpegCliArgument(), // add -vf yadif,nlmeans |
147
|
|
|
($threads === null) ? '' : sprintf('-threads %s', $threads), |
148
|
|
|
], $convertParams->getFFMpegArguments()); |
149
|
|
|
|
150
|
|
|
$pass1Cmd = implode(' ', array_merge( |
151
|
|
|
$commonArgs, |
152
|
|
|
[ |
153
|
|
|
'-pass 1', |
154
|
|
|
// tells VP9 to encode really fast, sacrificing quality. Useful to speed up the first pass. |
155
|
|
|
'-speed 4', |
156
|
|
|
'-y /dev/null', |
157
|
|
|
] |
158
|
|
|
)); |
159
|
|
|
|
160
|
|
|
$pass2Cmd = implode( ' ', array_merge( |
161
|
|
|
$commonArgs, |
162
|
|
|
[ |
163
|
|
|
'-pass 2', |
164
|
|
|
// speed 1 is a good speed vs. quality compromise. |
165
|
|
|
// Produces output quality typically very close to speed 0, but usually encodes much faster. |
166
|
|
|
'-speed 1', |
167
|
|
|
'-y', |
168
|
|
|
sprintf("%s", escapeshellarg($outputFile)) |
169
|
|
|
] |
170
|
|
|
)); |
171
|
|
|
|
172
|
|
|
|
173
|
|
|
$process = new Process($pass1Cmd); |
174
|
|
|
$process->setTimeout(null); |
175
|
|
|
$process->setIdleTimeout(60); // 60 seconds without output will stop the process |
176
|
|
|
$process->start(); |
177
|
|
|
foreach ($process as $type => $data) { |
178
|
|
|
if ($process::OUT === $type) { |
179
|
|
|
echo "\nRead from stdout: ".$data; |
180
|
|
|
} else { // $process::ERR === $type |
181
|
|
|
echo "\nRead from stderr: ".$data; |
182
|
|
|
} |
183
|
|
|
} |
184
|
|
|
|
185
|
|
|
$process = new Process($pass2Cmd); |
186
|
|
|
$process->setTimeout(null); |
187
|
|
|
$process->setIdleTimeout(60); // 60 seconds without output will stop the process |
188
|
|
|
$process->start(); |
189
|
|
|
foreach ($process as $type => $data) { |
190
|
|
|
if ($process::OUT === $type) { |
191
|
|
|
echo "\nRead from stdout: ".$data; |
192
|
|
|
} else { // $process::ERR === $type |
193
|
|
|
echo "\nRead from stderr: ".$data; |
194
|
|
|
} |
195
|
|
|
} |
196
|
|
|
|
197
|
|
|
} |
198
|
|
|
*/ |
199
|
|
|
} |
200
|
|
|
|
Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.
The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.
This check looks for comments that seem to be mostly valid code and reports them.