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\Filter\Video\EmptyVideoFilter; |
10
|
|
|
use Soluble\MediaTools\Filter\Video\VideoFilterChain; |
11
|
|
|
use Soluble\MediaTools\Filter\Video\VideoFilterInterface; |
12
|
|
|
use Soluble\MediaTools\Filter\Video\VideoFilterTypeDenoiseInterface; |
13
|
|
|
use Soluble\MediaTools\Filter\Video\YadifVideoFilter; |
14
|
|
|
use Symfony\Component\Process\Process; |
15
|
|
|
|
16
|
|
|
class VideoTranscode |
17
|
|
|
{ |
18
|
|
|
/** @var FFMpegConfig */ |
19
|
|
|
protected $ffmpegConfig; |
20
|
|
|
|
21
|
|
|
/** @var VideoProbe */ |
22
|
|
|
protected $videoProbe; |
23
|
|
|
|
24
|
1 |
|
public function __construct(FFMpegConfig $ffmpegConfig, VideoProbe $videoProbe) |
25
|
|
|
{ |
26
|
1 |
|
$this->videoProbe = $videoProbe; |
27
|
1 |
|
$this->ffmpegConfig = $ffmpegConfig; |
28
|
1 |
|
$this->ffmpegConfig->getProcess()->ensureBinaryExists(); |
29
|
1 |
|
} |
30
|
|
|
|
31
|
|
|
/* |
|
|
|
|
32
|
|
|
public function transcodeMultiPass(string $videoFile, string $outputFile, VideoTranscodeParams $transcodeParams, VideoFilterInterface $videoFilter=null): void { |
33
|
|
|
|
34
|
|
|
$this->ensureFileExists($videoFile); |
35
|
|
|
if ($videoFilter === null) { |
36
|
|
|
$videoFilter = new EmptyVideoFilter(); |
37
|
|
|
} |
38
|
|
|
|
39
|
|
|
|
40
|
|
|
$threads = $transcodeParams->getOption(VideoTranscodeParams::OPTION_THREADS, $this->ffmpegConfig->getThreads()); |
41
|
|
|
|
42
|
|
|
$ffmpegBin = $this->ffmpegConfig->getBinary(); |
43
|
|
|
|
44
|
|
|
$commonArgs = array_merge([ |
45
|
|
|
$ffmpegBin, |
46
|
|
|
sprintf('-i %s', escapeshellarg($videoFile)), // input filename |
47
|
|
|
$videoFilter->getFFMpegCliArgument(), // add -vf yadif,nlmeans |
48
|
|
|
($threads === null) ? '' : sprintf('-threads %s', $threads), |
49
|
|
|
], $transcodeParams->getFFMpegArguments()); |
50
|
|
|
|
51
|
|
|
$pass1Cmd = implode(' ', array_merge( |
52
|
|
|
$commonArgs, |
53
|
|
|
[ |
54
|
|
|
'-pass 1', |
55
|
|
|
// tells VP9 to encode really fast, sacrificing quality. Useful to speed up the first pass. |
56
|
|
|
'-speed 4', |
57
|
|
|
'-y /dev/null', |
58
|
|
|
] |
59
|
|
|
)); |
60
|
|
|
|
61
|
|
|
$pass2Cmd = implode( ' ', array_merge( |
62
|
|
|
$commonArgs, |
63
|
|
|
[ |
64
|
|
|
'-pass 2', |
65
|
|
|
// speed 1 is a good speed vs. quality compromise. |
66
|
|
|
// Produces output quality typically very close to speed 0, but usually encodes much faster. |
67
|
|
|
'-speed 1', |
68
|
|
|
'-y', |
69
|
|
|
sprintf("%s", escapeshellarg($outputFile)) |
70
|
|
|
] |
71
|
|
|
)); |
72
|
|
|
|
73
|
|
|
|
74
|
|
|
$process = new Process($pass1Cmd); |
75
|
|
|
$process->setTimeout(null); |
76
|
|
|
$process->setIdleTimeout(60); // 60 seconds without output will stop the process |
77
|
|
|
$process->start(); |
78
|
|
|
foreach ($process as $type => $data) { |
79
|
|
|
if ($process::OUT === $type) { |
80
|
|
|
echo "\nRead from stdout: ".$data; |
81
|
|
|
} else { // $process::ERR === $type |
82
|
|
|
echo "\nRead from stderr: ".$data; |
83
|
|
|
} |
84
|
|
|
} |
85
|
|
|
|
86
|
|
|
$process = new Process($pass2Cmd); |
87
|
|
|
$process->setTimeout(null); |
88
|
|
|
$process->setIdleTimeout(60); // 60 seconds without output will stop the process |
89
|
|
|
$process->start(); |
90
|
|
|
foreach ($process as $type => $data) { |
91
|
|
|
if ($process::OUT === $type) { |
92
|
|
|
echo "\nRead from stdout: ".$data; |
93
|
|
|
} else { // $process::ERR === $type |
94
|
|
|
echo "\nRead from stderr: ".$data; |
95
|
|
|
} |
96
|
|
|
} |
97
|
|
|
|
98
|
|
|
} |
99
|
|
|
*/ |
100
|
|
|
|
101
|
1 |
|
public function transcode(string $videoFile, string $outputFile, VideoTranscodeParams $transcodeParams, ?VideoFilterInterface $videoFilter = null): Process |
102
|
|
|
{ |
103
|
1 |
|
$this->ensureFileExists($videoFile); |
104
|
|
|
|
105
|
1 |
|
if ($videoFilter === null) { |
106
|
1 |
|
$videoFilter = new EmptyVideoFilter(); |
107
|
|
|
} |
108
|
|
|
|
109
|
1 |
|
$process = $this->ffmpegConfig->getProcess(); |
110
|
|
|
|
111
|
1 |
|
if (!$transcodeParams->hasOption(VideoTranscodeParams::OPTION_THREADS) && $this->ffmpegConfig->getThreads() !== null) { |
112
|
|
|
$transcodeParams = $transcodeParams->withThreads($this->ffmpegConfig->getThreads()); |
113
|
|
|
} |
114
|
|
|
|
115
|
1 |
|
$ffmpegCmd = $process->buildCommand( |
116
|
1 |
|
array_merge( |
117
|
|
|
[ |
118
|
1 |
|
sprintf('-i %s', escapeshellarg($videoFile)), // input filename |
119
|
1 |
|
$videoFilter->getFFMpegCLIArgument(), // add -vf yadif,nlmeans |
120
|
|
|
], |
121
|
1 |
|
$transcodeParams->getFFMpegArguments(), |
122
|
|
|
[ |
123
|
1 |
|
'-y', // tell to overwrite |
124
|
1 |
|
sprintf('%s', escapeshellarg($outputFile)), |
125
|
|
|
] |
126
|
|
|
) |
127
|
|
|
); |
128
|
|
|
|
129
|
1 |
|
$process = new Process($ffmpegCmd); |
130
|
1 |
|
$process->setTimeout(null); |
131
|
|
|
// 60 seconds without output will stop the process |
132
|
1 |
|
$process->setIdleTimeout(60); |
133
|
1 |
|
$process->start(); |
134
|
|
|
|
135
|
1 |
|
return $process; |
136
|
|
|
} |
137
|
|
|
|
138
|
|
|
/** |
139
|
|
|
* Try to guess if the original video is interlaced (bff, tff) and |
140
|
|
|
* return ffmpeg yadif filter argument and add denoise filter if any. |
141
|
|
|
* |
142
|
|
|
* @see https://ffmpeg.org/ffmpeg-filters.html (section yadif) |
143
|
|
|
* @see https://askubuntu.com/a/867203 |
144
|
|
|
* |
145
|
|
|
* @return VideoFilterInterface|VideoFilterChain|EmptyVideoFilter|YadifVideoFilter |
146
|
|
|
*/ |
147
|
|
|
public function getDeintFilter(string $videoFile, ?VideoFilterTypeDenoiseInterface $denoiseFilter = null): VideoFilterInterface |
148
|
|
|
{ |
149
|
|
|
$guess = $this->videoProbe->guessInterlacing($videoFile); |
150
|
|
|
$deintFilter = $guess->getDeinterlaceVideoFilter(); |
151
|
|
|
// skip all filters if video is not interlaces |
152
|
|
|
if ($deintFilter instanceof EmptyVideoFilter) { |
153
|
|
|
return $deintFilter; |
154
|
|
|
} |
155
|
|
|
if ($denoiseFilter !== null) { |
156
|
|
|
$videoFilterChain = new VideoFilterChain(); |
157
|
|
|
$videoFilterChain->addFilter($deintFilter); |
158
|
|
|
$videoFilterChain->addFilter($denoiseFilter); |
159
|
|
|
|
160
|
|
|
return $videoFilterChain; |
161
|
|
|
} |
162
|
|
|
|
163
|
|
|
return $deintFilter; |
164
|
|
|
} |
165
|
|
|
|
166
|
1 |
|
protected function ensureFileExists(string $file): void |
167
|
|
|
{ |
168
|
1 |
|
if (!file_exists($file)) { |
169
|
|
|
throw new FileNotFoundException(sprintf('File "%s" does not exists or is not readable', $file)); |
170
|
|
|
} |
171
|
1 |
|
} |
172
|
|
|
} |
173
|
|
|
|
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.