1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
/* |
4
|
|
|
* This class handled Video transcoding |
5
|
|
|
* We transcode the input file (S3 or HTTP) and generate output videos ad watermark |
6
|
|
|
* We use ffprobe, ffmpeg and convert to analyse, transcode and manipulate videos and images (watermark) |
7
|
|
|
* |
8
|
|
|
* Copyright (C) 2016 BFan Sports - Sport Archive Inc. |
9
|
|
|
* |
10
|
|
|
* This program is free software; you can redistribute it and/or modify |
11
|
|
|
* it under the terms of the GNU General Public License as published by |
12
|
|
|
* the Free Software Foundation; either version 2 of the License, or |
13
|
|
|
* (at your option) any later version. |
14
|
|
|
* |
15
|
|
|
* This program is distributed in the hope that it will be useful, |
16
|
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
17
|
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
18
|
|
|
* GNU General Public License for more details. |
19
|
|
|
* |
20
|
|
|
* You should have received a copy of the GNU General Public License along |
21
|
|
|
* with this program; if not, write to the Free Software Foundation, Inc., |
22
|
|
|
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. |
23
|
|
|
*/ |
24
|
|
|
|
25
|
|
|
require_once __DIR__ . '/BasicTranscoder.php'; |
26
|
|
|
|
27
|
|
|
use SA\CpeSdk; |
28
|
|
|
|
29
|
|
|
class VideoTranscoder extends BasicTranscoder |
30
|
|
|
{ |
31
|
|
|
// Errors |
32
|
|
|
const GET_VIDEO_INFO_FAILED = "GET_VIDEO_INFO_FAILED"; |
33
|
|
|
const GET_AUDIO_INFO_FAILED = "GET_AUDIO_INFO_FAILED"; |
34
|
|
|
const GET_DURATION_FAILED = "GET_DURATION_FAILED"; |
35
|
|
|
const NO_OUTPUT = "NO_OUTPUT"; |
36
|
|
|
const BAD_OUTPUT = "BAD_OUTPUT"; |
37
|
|
|
const NO_PRESET = "NO_PRESET"; |
38
|
|
|
const BAD_PRESETS_DIR = "BAD_PRESETS_DIR"; |
39
|
|
|
const UNKNOWN_PRESET = "UNKNOWN_PRESET"; |
40
|
|
|
const OPEN_PRESET_FAILED = "OPEN_PRESET_FAILED"; |
41
|
|
|
const BAD_PRESET_FORMAT = "BAD_PRESET_FORMAT"; |
42
|
|
|
const RATIO_ERROR = "RATIO_ERROR"; |
43
|
|
|
const ENLARGEMENT_ERROR = "ENLARGEMENT_ERROR"; |
44
|
|
|
const WATERMARK_ERROR = "WATERMARK_ERROR"; |
45
|
|
|
|
46
|
|
|
const SNAPSHOT_SEC_DEFAULT = 0; |
47
|
|
|
const INTERVALS_DEFAULT = 10; |
48
|
|
|
|
49
|
|
|
|
50
|
|
|
/*********************** |
51
|
|
|
* TRANSCODE INPUT VIDEO |
52
|
|
|
* Below is the code used to transcode videos based on $outputWanted. |
53
|
|
|
**********************/ |
54
|
|
|
|
55
|
|
|
// $metadata should contain the ffprobe video stream array. |
56
|
|
|
|
57
|
|
|
// Start FFmpeg for output transcoding |
58
|
|
|
public function transcode_asset( |
59
|
|
|
$tmpInputPath, |
60
|
|
|
$inputFilePath, |
61
|
|
|
$outputFilesPath, |
62
|
|
|
$metadata = null, |
63
|
|
|
$outputWanted) |
64
|
|
|
{ |
65
|
|
|
/* if (!$metadata) */ |
|
|
|
|
66
|
|
|
/* throw new CpeSdk\CpeException( */ |
|
|
|
|
67
|
|
|
/* "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.", */ |
68
|
|
|
/* self::TRANSCODE_FAIL */ |
69
|
|
|
/* ); */ |
70
|
|
|
|
71
|
|
|
if ($metadata) { |
72
|
|
|
// Extract an sanitize metadata |
73
|
|
|
$metadata = $this->_extractFileInfo($metadata); |
74
|
|
|
} |
75
|
|
|
|
76
|
|
|
$this->cpeLogger->logOut( |
77
|
|
|
"INFO", |
78
|
|
|
basename(__FILE__), |
79
|
|
|
"Start Transcoding Asset '$inputFilePath' ...", |
80
|
|
|
$this->logKey |
81
|
|
|
); |
82
|
|
|
|
83
|
|
View Code Duplication |
if ($metadata) |
|
|
|
|
84
|
|
|
$this->cpeLogger->logOut( |
85
|
|
|
"INFO", |
86
|
|
|
basename(__FILE__), |
87
|
|
|
"Input Video metadata: " . print_r($metadata, true), |
88
|
|
|
$this->logKey |
89
|
|
|
); |
90
|
|
|
|
91
|
|
|
try { |
92
|
|
|
$ffmpegCmd = ""; |
93
|
|
|
|
94
|
|
|
// Custom command |
95
|
|
|
if (isset($outputWanted->{'custom_cmd'}) && |
96
|
|
|
$outputWanted->{'custom_cmd'}) { |
97
|
|
|
$ffmpegCmd = $this->craft_ffmpeg_custom_cmd( |
98
|
|
|
$tmpInputPath, |
99
|
|
|
$inputFilePath, |
100
|
|
|
$outputFilesPath, |
101
|
|
|
$metadata, |
102
|
|
|
$outputWanted |
103
|
|
|
); |
104
|
|
|
} else if ($outputWanted->{'type'} == self::VIDEO) { |
105
|
|
|
$ffmpegCmd = $this->craft_ffmpeg_cmd_video( |
106
|
|
|
$tmpInputPath, |
107
|
|
|
$inputFilePath, |
108
|
|
|
$outputFilesPath, |
109
|
|
|
$metadata, |
110
|
|
|
$outputWanted |
111
|
|
|
); |
112
|
|
|
} else if ($outputWanted->{'type'} == self::THUMB) { |
113
|
|
|
$ffmpegCmd = $this->craft_ffmpeg_cmd_thumb( |
114
|
|
|
$tmpInputPath, |
115
|
|
|
$inputFilePath, |
116
|
|
|
$outputFilesPath, |
117
|
|
|
$metadata, |
118
|
|
|
$outputWanted |
119
|
|
|
); |
120
|
|
|
} |
121
|
|
|
|
122
|
|
|
$this->cpeLogger->logOut( |
123
|
|
|
"INFO", |
124
|
|
|
basename(__FILE__), |
125
|
|
|
"FFMPEG CMD:\n$ffmpegCmd\n", |
126
|
|
|
$this->logKey |
127
|
|
|
); |
128
|
|
|
|
129
|
|
|
// Send heartbeat and initialize progress |
130
|
|
|
$this->activityObj->activityHeartbeat( |
131
|
|
|
[ |
132
|
|
|
"output" => $outputWanted, |
133
|
|
|
"duration" => $metadata['duration'], |
134
|
|
|
"done" => 0, |
135
|
|
|
"progress" => 0 |
136
|
|
|
] |
137
|
|
|
); |
138
|
|
|
|
139
|
|
|
// Use executer to start FFMpeg command |
140
|
|
|
// Use 'capture_progression' function as callback |
141
|
|
|
// Pass video 'duration' as parameter |
142
|
|
|
// Sleep 1sec between turns and callback every 10 turns |
143
|
|
|
// Output progression logs (true) |
144
|
|
|
$this->executer->execute( |
145
|
|
|
$ffmpegCmd, |
146
|
|
|
1, |
147
|
|
|
array(2 => array("pipe", "w")), |
148
|
|
|
array($this, "capture_progression"), |
149
|
|
|
[ |
150
|
|
|
"duration" => $metadata['duration'], |
151
|
|
|
"output" => $outputWanted |
152
|
|
|
], |
153
|
|
|
true, |
154
|
|
|
10 |
155
|
|
|
); |
156
|
|
|
|
157
|
|
|
// Test if we have an output file ! |
158
|
|
|
if (!file_exists($outputFilesPath) || |
159
|
|
|
$this->isDirEmpty($outputFilesPath)) { |
160
|
|
|
throw new CpeSdk\CpeException( |
161
|
|
|
"Output file '$outputFilesPath' hasn't been created successfully or is empty !", |
162
|
|
|
self::TRANSCODE_FAIL |
163
|
|
|
); |
164
|
|
|
} |
165
|
|
|
|
166
|
|
|
// FFProbe the output file and return its information |
167
|
|
|
$outputInfo = $this->getAssetInfo($outputFilesPath."/".$outputWanted->{'output_file_info'}['basename']); |
168
|
|
|
} |
169
|
|
|
catch (\Exception $e) { |
170
|
|
|
$this->cpeLogger->logOut( |
171
|
|
|
"ERROR", |
172
|
|
|
basename(__FILE__), |
173
|
|
|
"Execution of command '".$ffmpegCmd."' failed: " . print_r($metadata, true). ". ".$e->getMessage(), |
174
|
|
|
$this->logKey |
175
|
|
|
); |
176
|
|
|
throw $e; |
177
|
|
|
} |
178
|
|
|
|
179
|
|
|
// No error. Transcode successful |
180
|
|
|
$this->cpeLogger->logOut( |
181
|
|
|
"INFO", |
182
|
|
|
basename(__FILE__), |
183
|
|
|
"Transcoding successfull !", |
184
|
|
|
$this->logKey |
185
|
|
|
); |
186
|
|
|
|
187
|
|
|
return [ |
188
|
|
|
"output" => $outputWanted, |
189
|
|
|
"outputInfo" => $outputInfo |
190
|
|
|
]; |
191
|
|
|
} |
192
|
|
|
|
193
|
|
|
// Craft custom command |
194
|
|
|
private function craft_ffmpeg_custom_cmd( |
195
|
|
|
$tmpInputPath, |
196
|
|
|
$inputFilePath, |
197
|
|
|
$outputFilesPath, |
198
|
|
|
$metadata, |
199
|
|
|
$outputWanted) |
200
|
|
|
{ |
201
|
|
|
$ffmpegCmd = $outputWanted->{'custom_cmd'}; |
202
|
|
|
|
203
|
|
|
// Replace ${input_file} by input file path |
204
|
|
|
$inputFilePath = escapeshellarg($inputFilePath); |
205
|
|
|
$ffmpegCmd = preg_replace('/\$\{input_file\}/', $inputFilePath, $ffmpegCmd); |
206
|
|
|
|
207
|
|
|
$watermarkOptions = ""; |
208
|
|
|
// Process options for watermark |
209
|
|
|
if (isset($outputWanted->{'watermark'}) && $outputWanted->{'watermark'}) { |
210
|
|
|
$watermarkOptions = |
211
|
|
|
$this->get_watermark_options( |
212
|
|
|
$tmpInputPath, |
213
|
|
|
$outputWanted->{'watermark'}); |
214
|
|
|
// Replace ${watermark_options} by watermark options |
215
|
|
|
$ffmpegCmd = preg_replace('/\$\{watermark_options\}/', $watermarkOptions, $ffmpegCmd); |
216
|
|
|
} |
217
|
|
|
|
218
|
|
|
// Append output filename to path |
219
|
|
|
$outputFilesPath .= "/" . $outputWanted->{'output_file_info'}['basename']; |
220
|
|
|
// Replace ${output_file} by output filename and path to local disk |
221
|
|
|
$ffmpegCmd = preg_replace('/\$\{output_file\}/', $outputFilesPath, $ffmpegCmd); |
222
|
|
|
|
223
|
|
|
return ($ffmpegCmd); |
224
|
|
|
} |
225
|
|
|
|
226
|
|
|
// Generate FFmpeg command for video transcoding |
227
|
|
|
private function craft_ffmpeg_cmd_video( |
228
|
|
|
$tmpInputPath, |
229
|
|
|
$inputFilePath, |
230
|
|
|
$outputFilesPath, |
231
|
|
|
$metadata, |
232
|
|
|
$outputWanted) |
233
|
|
|
{ |
234
|
|
|
// Check if a size is provided to override preset size |
235
|
|
|
$size = $this->set_output_video_size($metadata, $outputWanted); |
236
|
|
|
$inputFilePath = escapeshellarg($inputFilePath); |
237
|
|
|
|
238
|
|
|
$videoCodec = $outputWanted->{'preset_values'}->{'video_codec'}; |
239
|
|
|
if (isset($outputWanted->{'video_codec'})) { |
240
|
|
|
$videoCodec = $outputWanted->{'video_codec'}; |
241
|
|
|
} |
242
|
|
|
|
243
|
|
|
$audioCodec = $outputWanted->{'preset_values'}->{'audio_codec'}; |
244
|
|
|
if (isset($outputWanted->{'audio_codec'})) { |
245
|
|
|
$audioCodec = $outputWanted->{'audio_codec'}; |
246
|
|
|
} |
247
|
|
|
|
248
|
|
|
$videoBitrate = $outputWanted->{'preset_values'}->{'video_bitrate'}; |
249
|
|
|
if (isset($outputWanted->{'video_bitrate'})) { |
250
|
|
|
$videoBitrate = $outputWanted->{'video_bitrate'}; |
251
|
|
|
} |
252
|
|
|
|
253
|
|
|
$audioBitrate = $outputWanted->{'preset_values'}->{'audio_bitrate'}; |
254
|
|
|
if (isset($outputWanted->{'audio_bitrate'})) { |
255
|
|
|
$audioBitrate = $outputWanted->{'audio_bitrate'}; |
256
|
|
|
} |
257
|
|
|
|
258
|
|
|
$frameRate = $outputWanted->{'preset_values'}->{'frame_rate'}; |
259
|
|
|
if (isset($outputWanted->{'frame_rate'})) { |
260
|
|
|
$frameRate = $outputWanted->{'frame_rate'}; |
261
|
|
|
} |
262
|
|
|
|
263
|
|
|
$formattedOptions = ""; |
264
|
|
|
if (isset($outputWanted->{'preset_values'}->{'video_codec_options'})) { |
265
|
|
|
$formattedOptions = |
266
|
|
|
$this->set_output_video_codec_options($outputWanted->{'preset_values'}->{'video_codec_options'}); |
267
|
|
|
} |
268
|
|
|
|
269
|
|
|
$watermarkOptions = ""; |
270
|
|
|
// Process options for watermark |
271
|
|
|
if (isset($outputWanted->{'watermark'}) && $outputWanted->{'watermark'}) { |
272
|
|
|
$watermarkOptions = |
273
|
|
|
$this->get_watermark_options( |
274
|
|
|
$tmpInputPath, |
275
|
|
|
$outputWanted->{'watermark'}); |
276
|
|
|
} |
277
|
|
|
|
278
|
|
|
// Create FFMpeg arguments |
279
|
|
|
$ffmpegArgs = " -i $inputFilePath -y -threads 0"; |
280
|
|
|
$ffmpegArgs .= " -vf scale=$size"; |
281
|
|
|
$ffmpegArgs .= " -vcodec $videoCodec"; |
282
|
|
|
$ffmpegArgs .= " -acodec $audioCodec"; |
283
|
|
|
$ffmpegArgs .= " -b:v $videoBitrate"; |
284
|
|
|
$ffmpegArgs .= " -b:a $audioBitrate"; |
285
|
|
|
$ffmpegArgs .= " -r $frameRate"; |
286
|
|
|
$ffmpegArgs .= " $formattedOptions"; |
287
|
|
|
$ffmpegArgs .= " $watermarkOptions"; |
288
|
|
|
|
289
|
|
|
// Append output filename to path |
290
|
|
|
$outputFilesPath .= "/" . $outputWanted->{'output_file_info'}['basename']; |
291
|
|
|
// Final command |
292
|
|
|
$ffmpegCmd = "ffmpeg $ffmpegArgs $outputFilesPath"; |
293
|
|
|
|
294
|
|
|
return ($ffmpegCmd); |
295
|
|
|
} |
296
|
|
|
|
297
|
|
|
// Craft FFMpeg command to generate thumbnails |
298
|
|
|
private function craft_ffmpeg_cmd_thumb( |
299
|
|
|
$tmpInputPath, |
|
|
|
|
300
|
|
|
$inputFilePath, |
301
|
|
|
$outputFilesPath, |
302
|
|
|
$metadata, |
303
|
|
|
$outputWanted) |
304
|
|
|
{ |
305
|
|
|
// FIXME: Use $metadata to improve the FFMpeg command |
306
|
|
|
// inputAssetInfo contains FFprobe output |
307
|
|
|
|
308
|
|
|
$frameOptions = ""; |
309
|
|
|
$outputFileInfo = pathinfo($outputWanted->{'file'}); |
310
|
|
|
$inputFilePath = escapeshellarg($inputFilePath); |
311
|
|
|
if ($outputWanted->{'mode'} == 'snapshot') |
312
|
|
|
{ |
313
|
|
|
$snapshot_sec = self::SNAPSHOT_SEC_DEFAULT; |
314
|
|
|
if (isset($outputWanted->{'snapshot_sec'}) && |
315
|
|
|
$outputWanted->{'snapshot_sec'} > 0) { |
316
|
|
|
$snapshot_sec = $outputWanted->{'snapshot_sec'}; |
317
|
|
|
} |
318
|
|
|
|
319
|
|
|
$time = gmdate("H:i:s", $snapshot_sec) . ".000"; |
320
|
|
|
$outputFilesPath .= "/" . $outputFileInfo['basename']; |
321
|
|
|
$frameOptions = " -ss $time -vframes 1"; |
322
|
|
|
} |
323
|
|
|
else if ($outputWanted->{'mode'} == 'intervals') |
324
|
|
|
{ |
325
|
|
|
$intervals = self::INTERVALS_DEFAULT; |
326
|
|
|
if (isset($outputWanted->{'intervals'}) && |
327
|
|
|
$outputWanted->{'intervals'} > 0) { |
328
|
|
|
$intervals = $outputWanted->{'intervals'}; |
329
|
|
|
} |
330
|
|
|
|
331
|
|
|
$outputFilesPath .= "/" . $outputFileInfo['filename'] . "%06d." |
332
|
|
|
. $outputFileInfo['extension']; |
333
|
|
|
$frameOptions = " -vf fps=fps=1/$intervals"; |
334
|
|
|
} |
335
|
|
|
|
336
|
|
|
// Create FFMpeg arguments |
337
|
|
|
$ffmpegArgs = " -i $inputFilePath -y -threads 0"; |
338
|
|
|
$ffmpegArgs .= " -vf scale=" . $outputWanted->{'size'}; |
339
|
|
|
$ffmpegArgs .= " $frameOptions -f image2 -q:v 8"; |
340
|
|
|
|
341
|
|
|
// Final command |
342
|
|
|
$ffmpegCmd = "ffmpeg $ffmpegArgs $outputFilesPath"; |
343
|
|
|
|
344
|
|
|
return ($ffmpegCmd); |
345
|
|
|
} |
346
|
|
|
|
347
|
|
|
// Get watermark info to generate overlay options for ffmpeg |
348
|
|
|
private function get_watermark_options( |
349
|
|
|
$tmpInputPath, |
350
|
|
|
$watermarkOptions) |
351
|
|
|
{ |
352
|
|
|
// Get info about the video in order to save the watermark in same location |
353
|
|
|
$watermarkFileInfo = pathinfo($watermarkOptions->{'file'}); |
354
|
|
|
$watermarkPath = $tmpInputPath."/".$watermarkFileInfo['basename']; |
355
|
|
|
$newWatermarkPath = $tmpInputPath."/new-".$watermarkFileInfo['basename']; |
356
|
|
|
|
357
|
|
|
// Get watermark image from S3 |
358
|
|
|
$s3Output = $this->s3Utils->get_file_from_s3( |
359
|
|
|
$watermarkOptions->{'bucket'}, |
360
|
|
|
$watermarkOptions->{'file'}, |
361
|
|
|
$watermarkPath); |
362
|
|
|
|
363
|
|
|
$this->cpeLogger->logOut("INFO", |
364
|
|
|
basename(__FILE__), |
365
|
|
|
$s3Output['msg'], |
366
|
|
|
$this->logKey); |
367
|
|
|
|
368
|
|
|
// Transform watermark for opacity |
369
|
|
|
$convertCmd = "convert $watermarkPath -alpha on -channel A -evaluate Multiply " . $watermarkOptions->{'opacity'} . " +channel $newWatermarkPath"; |
370
|
|
|
|
371
|
|
|
try { |
372
|
|
|
$out = $this->executer->execute($convertCmd, 1, |
373
|
|
|
array(1 => array("pipe", "w"), 2 => array("pipe", "w")), |
374
|
|
|
false, false, |
375
|
|
|
false, 1); |
376
|
|
|
} |
377
|
|
|
catch (\Exception $e) { |
378
|
|
|
$this->cpeLogger->logOut( |
379
|
|
|
"ERROR", |
380
|
|
|
basename(__FILE__), |
381
|
|
|
"Execution of command '".$convertCmd."' failed", |
382
|
|
|
$this->logKey |
383
|
|
|
); |
384
|
|
|
return false; |
385
|
|
|
} |
386
|
|
|
|
387
|
|
|
// Any error ? |
388
|
|
|
if (isset($out['outErr']) && $out['outErr'] != "" && |
389
|
|
|
(!file_exists($newWatermarkPath) || !filesize($newWatermarkPath))) { |
390
|
|
|
throw new CpeSdk\CpeException( |
391
|
|
|
"Error transforming watermark file '$watermarkPath'!", |
392
|
|
|
self::WATERMARK_ERROR); |
393
|
|
|
} |
394
|
|
|
|
395
|
|
|
// Format options for FFMpeg |
396
|
|
|
$size = $watermarkOptions->{'size'}; |
397
|
|
|
$positions = $this->get_watermark_position($watermarkOptions); |
398
|
|
|
$formattedOptions = "-vf \"movie=$newWatermarkPath, scale=$size [wm]; [in][wm] overlay=" . $positions['x'] . ':' . $positions['y'] . " [out]\""; |
399
|
|
|
|
400
|
|
|
return ($formattedOptions); |
401
|
|
|
} |
402
|
|
|
|
403
|
|
|
// Generate the command line option to position the watermark |
404
|
|
|
private function get_watermark_position($watermarkOptions) |
405
|
|
|
{ |
406
|
|
|
$positions = array('x' => 0, 'y' => 0); |
407
|
|
|
|
408
|
|
|
if ($watermarkOptions->{'x'} >= 0) { |
409
|
|
|
$positions['x'] = $watermarkOptions->{'x'}; |
410
|
|
|
} |
411
|
|
|
if ($watermarkOptions->{'y'} >= 0) { |
412
|
|
|
$positions['y'] = $watermarkOptions->{'y'}; |
413
|
|
|
} |
414
|
|
|
if ($watermarkOptions->{'x'} < 0) { |
415
|
|
|
$positions['x'] = 'main_w-overlay_w' . $watermarkOptions->{'x'}; |
416
|
|
|
} |
417
|
|
|
if ($watermarkOptions->{'y'} < 0) { |
418
|
|
|
$positions['y'] = 'main_h-overlay_h' . $watermarkOptions->{'y'}; |
419
|
|
|
} |
420
|
|
|
|
421
|
|
|
return ($positions); |
422
|
|
|
} |
423
|
|
|
|
424
|
|
|
// Get Video codec options and format the options properly for ffmpeg |
425
|
|
|
private function set_output_video_codec_options($videoCodecOptions) |
426
|
|
|
{ |
427
|
|
|
$formattedOptions = ""; |
428
|
|
|
$options = explode(",", $videoCodecOptions); |
429
|
|
|
|
430
|
|
|
foreach ($options as $option) |
431
|
|
|
{ |
432
|
|
|
$keyVal = explode("=", $option); |
433
|
|
|
if ($keyVal[0] === 'Profile') { |
434
|
|
|
$formattedOptions .= " -profile:v ".$keyVal[1]; |
435
|
|
|
} else if ($keyVal[0] === 'Level') { |
436
|
|
|
$formattedOptions .= " -level ".$keyVal[1]; |
437
|
|
|
} else if ($keyVal[0] === 'MaxReferenceFrames') { |
438
|
|
|
$formattedOptions .= " -refs ".$keyVal[1]; |
439
|
|
|
} |
440
|
|
|
} |
441
|
|
|
|
442
|
|
|
return ($formattedOptions); |
443
|
|
|
} |
444
|
|
|
|
445
|
|
|
// Verify Ratio and Size of output file to ensure it respect restrictions |
446
|
|
|
// Return the output video size |
447
|
|
|
private function set_output_video_size(&$metadata, $outputWanted) |
448
|
|
|
{ |
449
|
|
|
// Handle video size |
450
|
|
|
$size = $outputWanted->{'preset_values'}->{'size'}; |
451
|
|
|
if (isset($outputWanted->{'size'})) { |
452
|
|
|
$size = $outputWanted->{'size'}; |
453
|
|
|
} |
454
|
|
|
|
455
|
|
|
// Ratio check |
456
|
|
|
if (!isset($outputWanted->{'keep_ratio'}) || |
457
|
|
|
$outputWanted->{'keep_ratio'} == 'true') |
458
|
|
|
{ |
459
|
|
|
// FIXME: Improve ratio check |
460
|
|
|
|
461
|
|
|
/* $outputRatio = floatval($this->get_ratio($size)); */ |
462
|
|
|
/* $inputRatio = floatval($metadata->{'ratio'}); */ |
463
|
|
|
|
464
|
|
|
/* if ($outputRatio != $inputRatio) */ |
|
|
|
|
465
|
|
|
/* throw new CpeSdk\CpeException( */ |
|
|
|
|
466
|
|
|
/* "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.", */ |
467
|
|
|
/* self::RATIO_ERROR */ |
468
|
|
|
/* ); */ |
469
|
|
|
} |
470
|
|
|
|
471
|
|
|
// Enlargement check |
472
|
|
|
if ($metadata && |
473
|
|
|
(!isset($outputWanted->{'allow_upscale'}) |
474
|
|
|
|| $outputWanted->{'allow_upscale'} == 'false')) |
475
|
|
|
{ |
476
|
|
|
$metadata['size'] = $metadata['video']['resolution']; |
477
|
|
|
$inputSize = $metadata['size']; |
478
|
|
|
$inputSizeSplit = explode("x", $inputSize); |
479
|
|
|
$outputSizeSplit = explode("x", $size); |
480
|
|
|
|
481
|
|
|
if (intval($outputSizeSplit[0]) > intval($inputSizeSplit[0]) || |
482
|
|
|
intval($outputSizeSplit[1]) > intval($inputSizeSplit[1])) { |
483
|
|
|
$this->cpeLogger->logOut( |
484
|
|
|
"INFO", |
485
|
|
|
basename(__FILE__), |
486
|
|
|
"Requested transcode size is bigger than the original. `allow_upscale` option not provided", |
487
|
|
|
$this->logKey |
488
|
|
|
); |
489
|
|
|
$size = $metadata['size']; |
490
|
|
|
} |
491
|
|
|
} |
492
|
|
|
|
493
|
|
|
return (str_replace("x",":", $size)); |
494
|
|
|
} |
495
|
|
|
|
496
|
|
|
// REad ffmpeg output and calculate % progress |
497
|
|
|
// This is a callback called from 'CommandExecuter.php' |
498
|
|
|
// $out and $outErr contain FFmpeg output |
499
|
|
|
public function capture_progression($params, $out, $outErr) |
500
|
|
|
{ |
501
|
|
|
$progress = 0; |
502
|
|
|
$done = 0; |
|
|
|
|
503
|
|
|
$duration = $params['duration']; |
504
|
|
|
$output = $params['output']; |
505
|
|
|
|
506
|
|
|
// # get the current time |
507
|
|
|
preg_match_all("/time=(.*?) bitrate/", $outErr, $matches); |
508
|
|
|
|
509
|
|
|
$last = array_pop($matches); |
510
|
|
|
// # this is needed if there is more than one match |
511
|
|
|
if (is_array($last)) { |
512
|
|
|
$last = array_pop($last); |
513
|
|
|
} |
514
|
|
|
|
515
|
|
|
// Perform Time transformation to get seconds |
516
|
|
|
$ar = array_reverse(explode(":", $last)); |
517
|
|
|
$done = floatval($ar[0]); |
518
|
|
|
if (!empty($ar[1])) { |
519
|
|
|
$done += intval($ar[1]) * 60; |
520
|
|
|
} |
521
|
|
|
if (!empty($ar[2])) { |
522
|
|
|
$done += intval($ar[2]) * 60 * 60; |
523
|
|
|
} |
524
|
|
|
|
525
|
|
|
// # finally, progress is easy |
526
|
|
|
if ($done && $duration) { |
527
|
|
|
$progress = round(($done/$duration)*100); |
528
|
|
|
} |
529
|
|
|
|
530
|
|
|
$this->cpeLogger->logOut( |
531
|
|
|
"INFO", |
532
|
|
|
basename(__FILE__), |
533
|
|
|
"Progress: $done / $progress%", |
534
|
|
|
$this->logKey |
535
|
|
|
); |
536
|
|
|
|
537
|
|
|
// Send heartbeat and progress data |
538
|
|
|
$this->activityObj->activityHeartbeat( |
539
|
|
|
[ |
540
|
|
|
"output" => $output, |
541
|
|
|
"duration" => $duration, |
542
|
|
|
"done" => $done, |
543
|
|
|
"progress" => $progress |
544
|
|
|
] |
545
|
|
|
); |
546
|
|
|
} |
547
|
|
|
|
548
|
|
|
// Combine preset and custom output settings to generate output settings |
549
|
|
|
public function get_preset_values($output_wanted) |
550
|
|
|
{ |
551
|
|
|
if (!$output_wanted) { |
552
|
|
|
throw new CpeSdk\CpeException("No output data provided to transcoder !", |
553
|
|
|
self::NO_OUTPUT); |
554
|
|
|
} |
555
|
|
|
|
556
|
|
|
if (!isset($output_wanted->{"preset"})) { |
557
|
|
|
throw new CpeSdk\CpeException("No preset selected for output !", |
558
|
|
|
self::BAD_PRESETS_DIR); |
559
|
|
|
} |
560
|
|
|
|
561
|
|
|
$preset = $output_wanted->{"preset"}; |
562
|
|
|
$presetPath = __DIR__ . '/../../../presets/'; |
563
|
|
|
|
564
|
|
|
if (!($presetContent = file_get_contents($presetPath.$preset.".json"))) { |
565
|
|
|
throw new CpeSdk\CpeException("Can't open preset file !", |
566
|
|
|
self::OPEN_PRESET_FAILED); |
567
|
|
|
} |
568
|
|
|
|
569
|
|
|
if (!($decodedPreset = json_decode($presetContent))) { |
570
|
|
|
throw new CpeSdk\CpeException("Bad preset JSON format !", |
571
|
|
|
self::BAD_PRESET_FORMAT); |
572
|
|
|
} |
573
|
|
|
|
574
|
|
|
return ($decodedPreset); |
575
|
|
|
} |
576
|
|
|
|
577
|
|
|
// Check if the preset exists |
578
|
|
|
public function validate_preset($output) |
579
|
|
|
{ |
580
|
|
|
if (!isset($output->{"preset"})) { |
581
|
|
|
throw new CpeSdk\CpeException("No preset selected for output !", |
582
|
|
|
self::BAD_PRESETS_DIR); |
583
|
|
|
} |
584
|
|
|
|
585
|
|
|
$preset = $output->{"preset"}; |
586
|
|
|
$presetPath = __DIR__ . '/../../../presets/'; |
587
|
|
|
|
588
|
|
|
if (!($files = scandir($presetPath))) { |
589
|
|
|
throw new CpeSdk\CpeException("Unable to open preset directory '$presetPath' !", |
590
|
|
|
self::BAD_PRESETS_DIR); |
591
|
|
|
} |
592
|
|
|
|
593
|
|
|
foreach ($files as $presetFile) |
594
|
|
|
{ |
595
|
|
|
if ($presetFile === '.' || $presetFile === '..') { continue; } |
596
|
|
|
|
597
|
|
|
if (is_file("$presetPath/$presetFile")) |
598
|
|
|
{ |
599
|
|
|
if ($preset === pathinfo($presetFile)["filename"]) |
600
|
|
|
{ |
601
|
|
|
if (!($presetContent = file_get_contents("$presetPath/$presetFile"))) { |
602
|
|
|
throw new CpeSdk\CpeException("Can't open preset file '$presetPath/$presetFile'!", |
603
|
|
|
self::OPEN_PRESET_FAILED); |
604
|
|
|
} |
605
|
|
|
|
606
|
|
|
if (!($decodedPreset = json_decode($presetContent))) { |
607
|
|
|
throw new CpeSdk\CpeException("Bad preset JSON format '$presetPath/$presetFile'!", |
608
|
|
|
self::BAD_PRESET_FORMAT); |
609
|
|
|
} |
610
|
|
|
|
611
|
|
|
return true; |
612
|
|
|
} |
613
|
|
|
} |
614
|
|
|
} |
615
|
|
|
|
616
|
|
|
throw new CpeSdk\CpeException("Unkown preset file '$preset' !", |
617
|
|
|
self::UNKNOWN_PRESET); |
618
|
|
|
} |
619
|
|
|
|
620
|
|
|
// Extract Metadata from ffprobe |
621
|
|
|
private function _extractFileInfo($metadata) { |
622
|
|
|
|
623
|
|
|
$videoStreams; |
624
|
|
|
$audioStreams; |
|
|
|
|
625
|
|
|
|
626
|
|
|
foreach ($metadata->streams as $key => $value) { |
627
|
|
|
if ($value->codec_type === 'video') { |
628
|
|
|
$videoStreams = $value; |
629
|
|
|
} |
630
|
|
|
else if ($value->codec_type === 'audio') { |
631
|
|
|
$audioStreams = $value; |
632
|
|
|
} |
633
|
|
|
} |
634
|
|
|
|
635
|
|
|
$analyse = [ |
636
|
|
|
'duration' => isset($metadata->format->duration) ? (float)$metadata->format->duration : 0, |
637
|
|
|
'video' => empty($videoStreams) ? null : [ |
638
|
|
|
'codec' => $videoStreams->codec_name, |
639
|
|
|
'color' => @$videoStreams->color_space, |
640
|
|
|
'resolution' => $videoStreams->width . 'x' . $videoStreams->height, |
641
|
|
|
'sar' => $videoStreams->sample_aspect_ratio, |
642
|
|
|
'dar' => $videoStreams->display_aspect_ratio, |
643
|
|
|
'framerate' => $videoStreams->r_frame_rate, |
644
|
|
|
'bitrate' => isset($videoStreams->bit_rate) ? (int)$videoStreams->bit_rate : null |
645
|
|
|
], |
646
|
|
|
'audio' => empty($audioStreams) ? null : [ |
647
|
|
|
'codec' => $audioStreams->codec_name, |
648
|
|
|
'frequency' => $audioStreams->sample_rate, |
649
|
|
|
'channels' => (int)$audioStreams->channels, |
650
|
|
|
'depth' => $audioStreams->bits_per_sample, |
651
|
|
|
'bitrate' => (int)$audioStreams->bit_rate |
652
|
|
|
] |
653
|
|
|
]; |
654
|
|
|
|
655
|
|
|
return $analyse; |
656
|
|
|
} |
657
|
|
|
} |
658
|
|
|
|
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.