1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
declare(strict_types=1); |
4
|
|
|
|
5
|
|
|
namespace Soluble\MediaTools\Video; |
6
|
|
|
|
7
|
|
|
use Soluble\MediaTools\Common\Assert\PathAssertionsTrait; |
8
|
|
|
use Soluble\MediaTools\Common\Exception\FileNotFoundException; |
9
|
|
|
use Soluble\MediaTools\Common\Exception\UnsupportedParamException; |
10
|
|
|
use Soluble\MediaTools\Common\Exception\UnsupportedParamValueException; |
11
|
|
|
use Soluble\MediaTools\Common\Process\ProcessParamsInterface; |
12
|
|
|
use Soluble\MediaTools\Config\FFMpegConfigInterface; |
13
|
|
|
use Soluble\MediaTools\Video\Adapter\FFMpegAdapter; |
14
|
|
|
use Soluble\MediaTools\Video\Exception\ConversionExceptionInterface; |
15
|
|
|
use Soluble\MediaTools\Video\Exception\ConversionProcessExceptionInterface; |
16
|
|
|
use Soluble\MediaTools\Video\Exception\InvalidParamException; |
17
|
|
|
use Soluble\MediaTools\Video\Exception\MissingInputFileException; |
18
|
|
|
use Soluble\MediaTools\Video\Exception\ProcessFailedException; |
19
|
|
|
use Soluble\MediaTools\Video\Exception\ProcessSignaledException; |
20
|
|
|
use Soluble\MediaTools\Video\Exception\ProcessTimedOutException; |
21
|
|
|
use Soluble\MediaTools\Video\Exception\RuntimeException; |
22
|
|
|
use Symfony\Component\Process\Exception as SPException; |
23
|
|
|
use Symfony\Component\Process\Process; |
24
|
|
|
|
25
|
|
|
class ConversionService implements ConversionServiceInterface |
26
|
|
|
{ |
27
|
|
|
use PathAssertionsTrait; |
28
|
|
|
|
29
|
|
|
/** @var ProcessParamsInterface */ |
30
|
|
|
protected $processParams; |
31
|
|
|
|
32
|
|
|
/** @var FFMpegAdapter */ |
33
|
|
|
protected $adapter; |
34
|
|
|
|
35
|
10 |
|
public function __construct(FFMpegConfigInterface $ffmpegConfig) |
36
|
|
|
{ |
37
|
10 |
|
$this->adapter = new FFMpegAdapter($ffmpegConfig); |
38
|
10 |
|
$this->processParams = $ffmpegConfig; |
39
|
10 |
|
} |
40
|
|
|
|
41
|
|
|
/** |
42
|
|
|
* Return ready-to-run symfony process object that you can use |
43
|
|
|
* to `run()` or `start()` programmatically. Useful if you want to make |
44
|
|
|
* things async... |
45
|
|
|
* |
46
|
|
|
* @see https://symfony.com/doc/current/components/process.html |
47
|
|
|
* |
48
|
|
|
* @throws UnsupportedParamException |
49
|
|
|
* @throws UnsupportedParamValueException |
50
|
|
|
*/ |
51
|
9 |
|
public function getSymfonyProcess(string $inputFile, string $outputFile, ConversionParamsInterface $convertParams, ?ProcessParamsInterface $processParams = null): Process |
52
|
|
|
{ |
53
|
9 |
|
if (!$convertParams->hasParam(ConversionParamsInterface::PARAM_THREADS) |
54
|
9 |
|
&& $this->adapter->getDefaultThreads() !== null) { |
55
|
1 |
|
$convertParams = $convertParams->withBuiltInParam( |
56
|
1 |
|
ConversionParamsInterface::PARAM_THREADS, |
57
|
1 |
|
$this->adapter->getDefaultThreads() |
58
|
|
|
); |
59
|
|
|
} |
60
|
|
|
|
61
|
9 |
|
$arguments = $this->adapter->getMappedConversionParams($convertParams); |
62
|
9 |
|
$ffmpegCmd = $this->adapter->getCliCommand($arguments, $inputFile, $outputFile); |
63
|
|
|
|
64
|
9 |
|
$pp = $processParams ?? $this->processParams; |
65
|
|
|
|
66
|
9 |
|
$process = new Process($ffmpegCmd); |
67
|
9 |
|
$process->setTimeout($pp->getTimeout()); |
68
|
9 |
|
$process->setIdleTimeout($pp->getIdleTimeout()); |
69
|
9 |
|
$process->setEnv($pp->getEnv()); |
70
|
|
|
|
71
|
9 |
|
return $process; |
72
|
|
|
} |
73
|
|
|
|
74
|
|
|
/** |
75
|
|
|
* Run a conversion, throw exception on error. |
76
|
|
|
* |
77
|
|
|
* @param callable|null $callback A PHP callback to run whenever there is some |
78
|
|
|
* tmp available on STDOUT or STDERR |
79
|
|
|
* |
80
|
|
|
* @throws ConversionExceptionInterface Base exception class for conversion exceptions |
81
|
|
|
* @throws ConversionProcessExceptionInterface Base exception class for process conversion exceptions |
82
|
|
|
* @throws MissingInputFileException |
83
|
|
|
* @throws ProcessTimedOutException |
84
|
|
|
* @throws ProcessFailedException |
85
|
|
|
* @throws ProcessSignaledException |
86
|
|
|
* @throws InvalidParamException |
87
|
|
|
* @throws RuntimeException |
88
|
|
|
*/ |
89
|
8 |
|
public function convert(string $inputFile, string $outputFile, ConversionParamsInterface $convertParams, ?callable $callback = null, ?ProcessParamsInterface $processParams = null): void |
90
|
|
|
{ |
91
|
|
|
try { |
92
|
8 |
|
$this->ensureFileExists($inputFile); |
93
|
7 |
|
$process = $this->getSymfonyProcess($inputFile, $outputFile, $convertParams, $processParams); |
94
|
7 |
|
$process->mustRun($callback); |
95
|
6 |
|
} catch (FileNotFoundException $e) { |
96
|
1 |
|
throw new MissingInputFileException($e->getMessage()); |
97
|
5 |
|
} catch (UnsupportedParamValueException | UnsupportedParamException $e) { |
98
|
|
|
throw new InvalidParamException($e->getMessage()); |
99
|
5 |
|
} catch (SPException\ProcessTimedOutException $e) { |
100
|
3 |
|
throw new ProcessTimedOutException($e->getProcess(), $e); |
101
|
2 |
|
} catch (SPException\ProcessSignaledException $e) { |
102
|
|
|
throw new ProcessSignaledException($e->getProcess(), $e); |
103
|
2 |
|
} catch (SPException\ProcessFailedException $e) { |
104
|
2 |
|
throw new ProcessFailedException($e->getProcess(), $e); |
105
|
|
|
} catch (SPException\RuntimeException $e) { |
106
|
|
|
throw new RuntimeException($e->getMessage()); |
107
|
|
|
} |
108
|
2 |
|
} |
109
|
|
|
|
110
|
|
|
/* |
|
|
|
|
111
|
|
|
* FOR LATER REFERENCE !!! |
112
|
|
|
public function convertMultiPass(string $videoFile, string $outputFile, ConversionParams $convertParams, VideoFilterInterface $videoFilter=null): void { |
113
|
|
|
|
114
|
|
|
$this->ensureFileExists($videoFile); |
115
|
|
|
if ($videoFilter === null) { |
116
|
|
|
$videoFilter = new EmptyVideoFilter(); |
117
|
|
|
} |
118
|
|
|
|
119
|
|
|
|
120
|
|
|
$threads = $convertParams->getOption(VideoConversionParams::OPTION_THREADS, $this->ffmpegConfig->getThreads()); |
121
|
|
|
|
122
|
|
|
$ffmpegBin = $this->ffmpegConfig->getBinary(); |
123
|
|
|
|
124
|
|
|
$commonArgs = array_merge([ |
125
|
|
|
$ffmpegBin, |
126
|
|
|
sprintf('-i %s', escapeshellarg($videoFile)), // input filename |
127
|
|
|
$videoFilter->getFFMpegCliArgument(), // add -vf yadif,nlmeans |
128
|
|
|
($threads === null) ? '' : sprintf('-threads %s', $threads), |
129
|
|
|
], $convertParams->getFFMpegArguments()); |
130
|
|
|
|
131
|
|
|
$pass1Cmd = implode(' ', array_merge( |
132
|
|
|
$commonArgs, |
133
|
|
|
[ |
134
|
|
|
'-pass 1', |
135
|
|
|
// tells VP9 to encode really fast, sacrificing quality. Useful to speed up the first pass. |
136
|
|
|
'-speed 4', |
137
|
|
|
'-y /dev/null', |
138
|
|
|
] |
139
|
|
|
)); |
140
|
|
|
|
141
|
|
|
$pass2Cmd = implode( ' ', array_merge( |
142
|
|
|
$commonArgs, |
143
|
|
|
[ |
144
|
|
|
'-pass 2', |
145
|
|
|
// speed 1 is a good speed vs. quality compromise. |
146
|
|
|
// Produces tmp quality typically very close to speed 0, but usually encodes much faster. |
147
|
|
|
'-speed 1', |
148
|
|
|
'-y', |
149
|
|
|
sprintf("%s", escapeshellarg($outputFile)) |
150
|
|
|
] |
151
|
|
|
)); |
152
|
|
|
|
153
|
|
|
|
154
|
|
|
$process = new Process($pass1Cmd); |
155
|
|
|
$process->setTimeout(null); |
156
|
|
|
$process->setIdleTimeout(60); // 60 seconds without tmp will stop the process |
157
|
|
|
$process->start(); |
158
|
|
|
foreach ($process as $type => $data) { |
159
|
|
|
if ($process::OUT === $type) { |
160
|
|
|
echo "\nRead from stdout: ".$data; |
161
|
|
|
} else { // $process::ERR === $type |
162
|
|
|
echo "\nRead from stderr: ".$data; |
163
|
|
|
} |
164
|
|
|
} |
165
|
|
|
|
166
|
|
|
$process = new Process($pass2Cmd); |
167
|
|
|
$process->setTimeout(null); |
168
|
|
|
$process->setIdleTimeout(60); // 60 seconds without tmp will stop the process |
169
|
|
|
$process->start(); |
170
|
|
|
foreach ($process as $type => $data) { |
171
|
|
|
if ($process::OUT === $type) { |
172
|
|
|
echo "\nRead from stdout: ".$data; |
173
|
|
|
} else { // $process::ERR === $type |
174
|
|
|
echo "\nRead from stderr: ".$data; |
175
|
|
|
} |
176
|
|
|
} |
177
|
|
|
|
178
|
|
|
} |
179
|
|
|
*/ |
180
|
|
|
} |
181
|
|
|
|
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.