FFMPEG Wrapperklasse
FFMPEG wrapper class
FFMPEG is a command line program for converting media files. If it is installed on your system you can use this swlib class for easy handling of ffmpeg in PHP. Supported is:
- Reading file information like meta data, codecs, data tracks, video width/height etc
- Modifying the container metadata
- Conversion of audio and video files.
Simply take a glance at the three example codes below ;)
FFMPEG ist ein Kommandozeilenprogramm zum Konvertieren von Mediadateien. Wenn es auf dem System installiert ist kann diese Klasse als leicht zu bedienende Schnittstelle für Handhabung damit sein. Unterstützt wird:
- Einlesen von Dateiinformationen, wie Metadaten, Codecs, Untertitel, Bildgröße, etc.
- Ändern der Metadaten des Containers
- Konvertieren von Video oder Audiodateien
Einfach ein Blick in die drei Beispiele werfen ;)
Sample source code 1: Read file info
Anwendungsbeispiel 1: Dateiinformationen lesen
<?
//
// Example 1: Getting file infos
//
require_once 'swlib/swlib.class.php';
use sw\FfmpegFile;
use sw\FileSystem as fs;
//
// Configuration, shown here are the defaults, which you can overwrite.
// If ffmpeg_bin is "", then the class assumes that ffmpeg is in the execution
// path.
//
FfmpegFile::config(array(
'ffmpeg_bin' => '', // path to ffmpeg binary
'time-limit' => 0, // for server: php script time limit
'formula-dir' => null, // where to find the conversion/trnascoding formulas?
'default-formula' => 'mp4-h264-aac', // what to convert is no formula is given?
));
//
// Ok lets scan a directory and show what MP4s we have there:
//
foreach(glob('*.mp4') as $video) {
print "#######################################################\n";
print "### VIDEO $video\n";
print "#######################################################\n";
// Instantiate
$ffmpeg = new FfmpegFile($video);
print "Container format = " . $ffmpeg->getContainerFormat() . "\n";
// Show Metadata
$meta = $ffmpeg->getContainerMetaData();
print "\n";
print "Metadata = {\n";
foreach($meta as $k => $v) print " $k = $v\n";
print "}\n";
// Streams
print "\n";
print "Number of video streams = " . $ffmpeg->getVideoStreamCount() . "\n";
print "Number of audio streams = " . $ffmpeg->getAudioStreamCount() . "\n";
print "Number of data streams = " . $ffmpeg->getDataStreamCount() . "\n";
print "\n";
print "Video bitrate = " . $ffmpeg->getVideoBitrate() . "\n";
print "Video codec = " . $ffmpeg->getVideoCodec() . "\n";
print "Video FPS = " . $ffmpeg->getVideoFramesPerSecond() . "\n";
print "Video width = " . $ffmpeg->getVideoWidth() . "\n";
print "Video height = " . $ffmpeg->getVideoHeight() . "\n";
// Audio streams
print "\n";
for($i=0; $i<$ffmpeg->getAudioStreamCount(); $i++) {
print "Audio stream $i = {\n";
print " Codec = " . $ffmpeg->getAudioCodec() . "\n";
print " Bitrate = " . $ffmpeg->getAudioBitrate($i) . "\n";
print " Channels = " . $ffmpeg->getAudioChannelCount($i) . "\n";
print "}\n";
}
// Data streams
print "\n";
for($i=0; $i<$ffmpeg->getDataStreamCount(); $i++) {
print "Data stream $i = {\n";
print_r($ffmpeg->getDataStreamInfo($i));
print "}\n";
}
}
#######################################################
### VIDEO JourneyQuest S01E01 - Onward.mp4
#######################################################
Container format = mov,mp4,m4a,3gp,3g2,mj2
Metadata = {
major_brand = mp42
minor_version = 0
compatible_brands = isommp42
creation_time = 2010-09-27 04:59:24
}
Number of video streams = 1
Number of audio streams = 1
Number of data streams = 0
Video bitrate = 3946000
Video codec = h264
Video FPS = 23.98
Video width = 1920
Video height = 1080
Audio stream 0 = {
Codec = aac
Bitrate = 127000
Channels = 2
}
#######################################################
### VIDEO JourneyQuest S01E02 - Sod the Quest.mp4
#######################################################
Container format = mov,mp4,m4a,3gp,3g2,mj2
[...]
Sample source code 2: Change meta data
Anwendungsbeispiel 2: Metadaten ändern
<?
//
// Example2: Setting meta data
//
require_once 'swlib/swlib.class.php';
use sw\FfmpegFile;
$file = 'JourneyQuest S01E01 - Onward.mp4';
$ffmpeg = new FfmpegFile($file);
$meta = $ffmpeg->getContainerMetaData();
unset($ffmpeg);
print "Metadata before changing = " . print_r($meta, true) . "\n";
//
// Now we edit them ..
//
$meta['title'] = 'JourneyQuest S01E01 - Onward';
$meta['description'] = 'The story begins ...';
$meta['artist'] = 'Dead Gentlemen';
$meta['comment'] = 'Some comments';
$meta['genre'] = 'TV & Film';
FfmpegFile::changeFileMetadata($file, $meta);
unset($meta);
//
// And double check by reloading the file:
//
$ffmpeg = new FfmpegFile($file);
$meta = $ffmpeg->getContainerMetaData();
print "Metadata after changing = " . print_r($meta, true) . "\n";
$ php ffmpegwrapper2.test.php
Metadata before changing = Array
(
[major_brand] => mp42
[minor_version] => 0
[compatible_brands] => isommp42
[creation_time] => 2010-09-27 04:59:24
)
Metadata after changing = Array
(
[major_brand] => isom
[minor_version] => 512
[compatible_brands] => isomiso2avc1mp41
[creation_time] => 2010-09-27 04:59:24
[title] => JourneyQuest S01E01 - Onward
[artist] => Dead Gentlemen
[encoder] => Lavf54.63.104
[comment] => Some comments
[genre] => TV & Film
[description] => The story begins ...
)
$ ffmpeg -i JourneyQuest\ S01E01\ -\ Onward.mp4
ffmpeg version 1.2.1 Copyright (c) 2000-2013 the FFmpeg developers
[...]
Input #0, mov,mp4,m4a,3gp,3g2,mj2, from 'JourneyQuest S01E01 - Onward.mp4':
Metadata:
major_brand : isom
minor_version : 512
compatible_brands: isomiso2avc1mp41
creation_time : 2010-09-27 04:59:24
title : JourneyQuest S01E01 - Onward
artist : Dead Gentlemen
encoder : Lavf54.63.104
comment : Some comments
genre : TV & Film
description : The story begins ...
Duration: 00:07:34.37, start: 0.000000, bitrate: 4081 kb/s
Stream #0:0(und): Video: h264 (High) (avc1 / 0x31637661), yuv420p, ...
Metadata:
creation_time : 2010-09-27 04:59:24
handler_name : VideoHandler
Stream #0:1(und): Audio: aac (mp4a / 0x6134706D), 44100 Hz, stereo, fltp, 127 kb/s
Metadata:
creation_time : 2010-09-27 04:59:24
handler_name : SoundHandler
At least one output file must be specified
Sample source code 3: Convert video format
Here to H264, AAC in a MP4 container.
Anwendungsbeispiel 3: Konvertieren
Hier in H264, AAC in einem MP4 container.
<?
//
// Example3: Convert a file
//
require_once 'swlib/swlib.class.php';
use sw\FfmpegFile;
//
// Easy going. Note: The formula name corresponds to a file
// 'ffmpeg-mp4-h264-aac.php' in the folder swlib/media/formulas.
// You can add own formulas there or somewhere else and specify your own
// formula folder using FfmpegFile::config();
//
FfmpegFile::convertFile(
'video.avi', // input file
'video.mp4', // output file
'mp4-h264-aac' // Formula name
);
//
// Example formula file:
//
class my_ffmpeg_copy extends sw\FfmpegFormula {
public function getDescription() {
return "Does not convert any streams, copies instead.";
}
public function getOutputFileExtension() {
return '';
}
public function getArguments() {
return array(
array('-vcodec' => 'copy'),
array('-acodec' => 'copy')
);
}
}
Class source code
Klassen-Quelltext
<?php
/**
* FFmpeg exceptions
* and meta information.
*
* @gpackage de.atwillys.sw.php.swLib
* @author Stefan Wilhelm
* @copyright Stefan Wilhelm, 2007-2012
* @license GPL
* @version 1.0
*/
namespace sw;
class FfmpegException extends LException {
}
<?php
/**
* FFmpeg conversion formula base class
*
* @gpackage de.atwillys.sw.php.swLib
* @author Stefan Wilhelm
* @copyright Stefan Wilhelm, 2007-2012
* @license GPL
* @version 1.0
*/
namespace sw;
require_once('FfmpegException.class.php');
abstract class FfmpegFormula {
/**
* Contains the file info of the input file
* @var array
*/
protected $inputFileInfo = array();
/**
* The arguments that the user passed to the transcoding function
* @var array
*/
protected $userArgs = array();
/**
* Supported formats
* @var array
*/
protected $formats = array();
/**
* Returns what it converts to
* @rerurn string
*/
public abstract function getDescription();
/**
* Returns which extension the container has
*/
public abstract function getOutputFileExtension();
/**
* Returns the arguments for the conversion excluding the '-i' paramter
* @return array
*/
public abstract function getArguments();
/**
* Constructor
* @param array $inputFileInfo
* @param array $userArgs
*/
public final function __construct($inputFileInfo=array(), $userArgs=array(), $formats=array()) {
$this->inputFileInfo = $inputFileInfo;
$this->userArgs = $userArgs;
$this->formats = $formats;
}
}
<?php
/**
* FFmpeg handling and conversion class
*
* @gpackage de.atwillys.sw.php.swLib
* @author Stefan Wilhelm
* @copyright Stefan Wilhelm, 2007-2012
* @license GPL
* @version 1.0
*/
namespace sw;
require_once('FfmpegException.class.php');
require_once('FfmpegFormula.class.php');
class FfmpegFile {
/**
* Class configuration
* @var array
*/
protected static $config = array(
// path to ffmpeg binary
'ffmpeg_bin' => '',
// for server: php script time limit
'time-limit' => 0,
// where to find the conversion/trnascoding formulas?
'formula-dir' => null,
// what to convert is no formula is given?
'default-formula' => 'mp4-h264-aac',
);
/**
* Key-value stored format list, entry e.g.:
* [mpeg] => Array
* (
* [d] => 1
* [e] => 1
* [info] => MPEG-1 System format
* )
* , where d=decoder available, e=encoder available, info=info text
* @var array
*/
private static $supportedFormats = null;
/**
* Stores the pixel supported formats
* @var array
*/
private static $supportedPixelFormats = null;
/**
* Key-value stored header parsed from the ffmpeg execution. Contains
* 'version', 'build-info', 'build-configuration', 'libs'.
* @var array
*/
protected static $ffmpegInfo = array();
/**
* The file path
* @var string
*/
protected $filePath = '';
/**
* File info read using readFileInfo(). Updated automatically if setPath()
* is called.
* @var array
*/
protected $fileInfo = array();
/**
* Function/method reference to the progress callback. The callback
* function(int $progress) { ... }
* @var mixed
*/
protected $progressCallback = null;
/**
* Returns the class configuration. If a configuration array is given, modifies
* the configuration by key merging.
* @param array $config
* @return array
*/
public static final function config(array $config = array()) {
if (!empty($config)) {
self::$config = array_merge(self::$config, $config);
Tracer::trace_r($config, '$config', 3);
}
if (empty(self::$config['formula-dir'])) {
self::$config['formula-dir'] = dirname(__FILE__) . '/formulas';
}
if (empty(self::$config['ffmpeg_bin']) || !FileSystem::isFile(self::$config['ffmpeg_bin']) || !FileSystem::isExecutable(self::$config['ffmpeg_bin'])) {
$ffmpeg = trim(exec('which ffmpeg'), "\t\n\r ");
Tracer::trace("ffmpeg binary search result (which ffmpeg)=$ffmpeg", 3);
if (empty($ffmpeg)) {
throw new FfmpegException("Your configutation is incorrect: can't find the ffmpeg binary: :binary", array(':binary' => self::$config['ffmpeg_bin']));
} else {
Tracer::trace("Warning: The configured ffmpeg is wrong (" . self::$config['ffmpeg_bin'] . "), but found binary '$ffmpeg'", 3);
}
self::$config['ffmpeg_bin'] = $ffmpeg;
}
return self::$config;
}
/**
* Runs ffmpeg with the given parameters, each is an array with one assoc entry
* key=>value, like array( array("-i" => $myfile), array('-metadata' => 'a=b') ).
* Automatically escapes value shell arguments (only values, not the keys).
* @param array $args
* @param string $outFile
* @param mixed $onOutputCallback=null
* @return array
*/
public static function executeFfmpeg(array $args = array(), $outFile = null, $onOutputCallback = null) {
self::config();
$cmd = self::$config['ffmpeg_bin'];
Tracer::trace_r($args, 'args', 3);
if (empty($args)) {
$args = array();
}
foreach ($args as $vk) {
if (!is_array($vk)) {
throw new FfmpegException("Shell args array values must be associative arrays with one element");
} else {
$v = reset($vk);
$k = key($vk);
}
if (is_null($v)) {
$v = '';
} else if (is_numeric($v)) {
$v = " $v";
} else if (empty($v)) {
$v = " ''";
} else if (preg_replace('/[\d\w\-]/i', '', $v) == '') {
$v = " $v";
} else {
$v = ' ' . escapeshellarg($v);
}
$cmd .= ' ' . $k . $v;
}
if (!empty($outFile)) {
$cmd .= ' ' . escapeshellarg(trim($outFile));
}
$cmd .= ' 2>&1';
Tracer::trace_r($cmd, 'shell command', 3);
// Run
$proc = new ShellProcess();
$proc->setCommand($cmd);
$proc->setCallbacks(array('onStdOut' => empty($onOutputCallback) ? null : $onOutputCallback));
$proc->setTerminateOnAbort(true);
$proc->setTimeout(self::$config['time-limit']);
$proc->setFetchOutput(true);
$proc->run();
$stdout = explode("\n", trim($proc->getStdOut(), "\r\n\t "));
$exitCode = $proc->getExitCode();
// exec($cmd, $stdout, $exitCode);
// Initial check
if (empty($stdout)) {
throw new FfmpegException("Failed to parse ffmpeg output: ffmpeg binary did not return any output");
}
// Separate header
foreach ($stdout as $k => $v) {
$stdout[$k] = rtrim(str_replace(array("\r", "\t"), array('', ' '), $v));
}
$header = array();
while(!empty($stdout)) {
if(stripos(reset($stdout), 'Input #') === 0) {
break;
}
$header[] = array_shift($stdout);
}
$header = array_filter($header);
// Header line 1 is like 'ffmpeg version x.x.x, Copyright (c) 2000-20xx the FFmpeg developers'
if (stripos(reset($header), 'ffmpeg') === 0) {
$r = strtolower(array_shift($header));
self::$ffmpegInfo['version'] = trim(reset(explode(',', str_replace(array('ffmpeg', 'version'), '', $r))));
array_shift($vk);
// Header line 2 is like ' built on Month Day Year t:i:me with gcc x.x.x ([...]]. build [nnnn]) (dot 3'
$r = trim(array_shift($header));
if (strpos(strtolower($r), 'built') !== 0) {
Tracer::trace("Ffmpeg info header: Expected the second line of ffmpeg to start with 'built on [...]'", 4);
self::$ffmpegInfo['build-info'] = '';
} else {
self::$ffmpegInfo['build-info'] = trim(str_replace(array('built', ' on'), '', $r));
}
// Header line 3 is like 'configuration: --prefix=[...] --enable-shared --enable-gpl [...]'
$r = trim(array_shift($header));
if (strpos(strtolower($r), 'configuration') !== 0) {
Tracer::trace("Failed to parse ffmpeg output: Expected the 3rd line of ffmpeg to start with 'configuration: [...]'", 4);
self::$ffmpegInfo['build-configuration'] = '';
} else {
self::$ffmpegInfo['build-configuration'] = trim(str_replace('configuration:', '', $r));
}
// Header lines following are the lib versions
self::$ffmpegInfo['libs'] = array();
while (!empty($header) && strlen(trim(reset($header))) != 0 && strpos(reset($stdout), ' ') === 0) {
$r = explode(' ', trim(array_shift($header)), 2);
if (count($r) != 2) {
Tracer::trace("Failed to parse ffmpeg output: Expected library entry to contain spaces", 4);
}
self::$ffmpegInfo['libs'][trim(strtolower(reset($r)))] = str_replace(' ', '', strtolower(end($r)));
}
}
return array(
'command' => $cmd,
'stdout' => $stdout,
'exitcode' => $exitCode
);
}
/**
* Returns the available formats as assoc. array containing the keys:
* 'version', 'build-configuration', 'build-info', (array) 'libs'.
* @return array
*/
public static function getSupportedFormats() {
if (is_null(self::$supportedFormats)) {
// Looks like:
// HEADER
//File formats:
// D. = Demuxing supported
// .E = Muxing supported
// --
// E 3g2 3GP2 format
$formats = array();
$r = self::executeFfmpeg(array(array('-formats' => null)));
$r = $r['stdout'];
// Remove everything until the "--" line, including this line itself
for ($v = reset($r); !empty($r) && strpos($v, '--') === false; $v = array_shift($r))
;
foreach ($r as $k => $v) {
while (strpos($v, ' ') !== false) {
$v = trim(str_replace(' ', ' ', $v));
}
$r[$k] = explode(' ', $v, 3);
}
foreach ($r as $k => $v) {
if (count($v) == 3) {
$formats[$v[1]] = array(
'd' => strpos(strtolower($v[0]), 'd') !== false,
'e' => strpos(strtolower($v[0]), 'e') !== false,
'info' => $v[2]
);
}
}
self::$supportedFormats = $formats;
}
return self::$supportedFormats;
}
/**
* Returns the available formats as assoc. array containing the keys:
* 'version', 'build-configuration', 'build-info', (array) 'libs'.
* @return array
*/
public static function getSupportedPixelFormats() {
if (is_null(self::$supportedPixelFormats)) {
// Looks like:
// HEADER
// Pixel formats:
// I.... = Supported Input format for conversion
// .O... = Supported Output format for conversion
// ..H.. = Hardware accelerated format
// ...P. = Paletted format
// ....B = Bitstream format
// FLAGS NAME NB_COMPONENTS BITS_PER_PIXEL
// -----
// IO... yuv420p 3 12
$formats = array();
$r = self::executeFfmpeg(array(array('-pix_fmts' => null)));
$r = $r['stdout'];
// Remove everything until the "--" line, including this line itself
for ($v = reset($r); !empty($r) && strpos($v, '----') === false; $v = array_shift($r))
;
foreach ($r as $k => $v) {
while (strpos($v, ' ') !== false) {
$v = trim(str_replace(' ', ' ', $v));
}
$r[$k] = explode(' ', $v, 4);
}
foreach ($r as $k => $v) {
if (count($v) == 4) {
$formats[strtolower($v[1])] = array(
'name' => $v[1],
'i' => strpos(strtolower($v[0]), 'i') !== false,
'o' => strpos(strtolower($v[0]), 'o') !== false,
'h' => strpos(strtolower($v[0]), 'h') !== false,
'p' => strpos(strtolower($v[0]), 'p') !== false,
'b' => strpos(strtolower($v[0]), 'b') !== false,
'components' => $v[2],
'bits-per-pixel' => $v[3],
);
}
}
self::$supportedPixelFormats = $formats;
}
return self::$supportedPixelFormats;
}
/**
* Returns information about a file
* @param string $file
*/
public static function readFileInfo($file) {
self::getSupportedPixelFormats(); // Initialize self::$supportedPixelFormats
Tracer::trace("\$file=$file", 3);
if (!is_file($file)) {
throw new FfmpegException("Cannot get file info, file does not exist: ':file'", array('file' => $file));
}
$return = self::executeFfmpeg(array(array('-i' => $file)));
$return = $stdout = $return['stdout'];
$possibleErrorMessage = end($return);
$returnRemaining = $fi = array();
try {
while (!empty($return) && strpos(trim(reset($return)), 'Input') !== 0) {
array_shift($return);
}
if (empty($return)) {
throw new FfmpegException("Failed to read file info: Missing 'Input #0', ffmpeg last output line is ':t'", array(':t' => $possibleErrorMessage));
}
// Line: Input #x, [format, can contain "m"], from 'file':
$r = explode(',', array_shift($return));
if (count($r) < 3) {
throw new FfmpegException("Failed to read file info: Output should have at least 2 commas (line=:line)", array('line' => implode(',', $r)));
} else {
// remove 'Input #x'
array_shift($r);
// Get file
$fi['file'] = array_pop($r);
if (strpos($fi['file'], 'from') === false) {
throw new FfmpegException("Failed to read file info: Expected 'from' in input file stat");
}
$fi['file'] = trim(preg_replace('/from/', '', $fi['file'], 1), " :'");
// Get format by reassembling the remaining data
$fi['format'] = trim(implode(',', $r));
}
// The next line can be the metadata of the file
if (strpos(strtolower(reset($return)), 'metadata') !== false) {
$r = array_shift($return);
$fi['meta'] = array();
$indent = strlen($r) - strlen(ltrim($r));
if ($indent <= 0) {
throw new FfmpegException("Failed to read file info: Expected metadata indentation");
}
while (!empty($return) && strlen(reset($return)) - strlen(ltrim(reset($return))) > $indent) {
$r = explode(':', array_shift($return), 2);
$fi['meta'][trim(reset($r))] = trim(end($r));
}
}
// The next line can be the stream information
if (strpos(strtolower(reset($return)), 'duration') !== false) {
$r = array_shift($return);
$indent = strlen($r) - strlen(ltrim($r));
if ($indent <= 0) {
throw new FfmpegException("Failed to read file info: Expected metadata indentation");
}
// E.g.: 'Duration: 00:10:00.00, start: 0.000000, bitrate: 1000 kb/s'
$r = explode(',', $r);
foreach ($r as $v) {
$v = explode(':', $v, 2);
$fi[strtolower(trim($v[0]))] = trim($v[1]);
}
$fi['streams'] = $fi['chapters'] = array();
while (!empty($return) && strlen(reset($return)) - strlen(ltrim(reset($return))) > $indent) {
$r = explode(':', array_shift($return), 3);
$indent2 = strlen($r[0]) - strlen(ltrim($r[0]));
$r[0] = strtolower(trim($r[0]));
if (strpos($r[0], 'stream') === 0) {
// Could be as well: Stream #0:0(und): Video
// ^
// |
// This is problematic for explode, so we shift this to:
// Stream #0.0: Video
//
if (is_numeric(substr(trim($r[1]), 0, 1))) {
$r[0] .= '.' . trim($r[1]);
$r[1] = explode(':', $r[2], 2);
$r[2] = $r[1][1];
$r[1] = $r[1][0];
}
// Stream declaration
$details = explode(',', trim($r[2]));
foreach ($details as $k => $v)
$details[$k] = trim($v);
// Determine stream name for key
$k = count($fi['streams']); // preg_replace('/^stream[\s]*#([\d\.]+).*/i', '${1}', $r[0]);
$fi['streams'][$k] = array();
$rfi = &$fi['streams'][$k];
// Stream name is like "stream #0.0(eng) [...]"
$rfi['name'] = strtolower(trim($r[0]));
// Stream type is like 'video', 'audio', 'data' ...
$rfi['type'] = strtolower(trim($r[1]));
// Stream codec is like
$rfi['codec'] = strtolower(trim(reset(explode(' ', trim(reset(explode('/', $details[0])))))));
// Analyze details
array_shift($details);
if ($rfi['type'] == 'video') {
foreach ($details as $tk => $tv) {
$tv = strtolower($tv);
if (strpos($tv, 'b/s') !== false) {
$rfi['bitrate'] = trim(str_replace(array(' ', 'b/s'), '', $tv));
$details[$tk] = null;
} else if (strpos($tv, 'fps') !== false) {
$rfi['fps'] = trim(reset(explode(' ', $tv)));
$details[$tk] = null;
} else if (strpos($tv, 'tbr') !== false) {
$rfi['tbr'] = trim(reset(explode(' ', $tv)));
$details[$tk] = null;
} else if (strpos($tv, 'tbc') !== false) {
$rfi['tbc'] = trim(reset(explode(' ', $tv)));
$details[$tk] = null;
} else if (strpos($tv, 'tbn') !== false) {
$rfi['tbn'] = trim(reset(explode(' ', $tv)));
$details[$tk] = null;
} else if (preg_match('/^[\d]+x[\d]+/', $tv)) {
$tv = explode('x', reset(explode(' ', $tv)));
if (!is_numeric($tv[0]) || !is_numeric($tv[1])) {
throw new FfmpegException("Failed to read file info: Video sixe components expected to be numeric");
}
$rfi['width'] = intval($tv[0]);
$rfi['height'] = intval($tv[1]);
$details[$tk] = null;
} else if (isset(self::$supportedPixelFormats[strtolower($tv)])) {
$rfi['pixelformat'] = $tv;
$details[$tk] = null;
} else {
$details[$tk] = null;
$details[strval($tk)] = str_replace('/', '', $tv);
}
}
if (!isset($rfi['pixelformat'])) {
$rfi['pixelformat'] = '';
}
} else if ($rfi['type'] == 'audio') {
foreach ($details as $tk => $tv) {
$tv = strtolower($tv);
if (strpos($tv, 'hz') !== false) {
$rfi['sample-rate'] = trim(str_replace('hz', '', $tv));
$details[$tk] = null;
} else if (strpos($tv, 'stereo') !== false) {
$rfi['channels'] = 2;
$details[$tk] = null;
} else if (strpos($tv, 'mono') !== false) {
$rfi['channels'] = 1;
$details[$tk] = null;
} else if (strpos($tv, 'channels') !== false) {
$rfi['channels'] = trim(str_replace('channels', '', $tv));
$details[$tk] = null;
} else if (preg_match('/^s[\d]+$/', $tv)) {
$rfi['sample-bits'] = str_replace('s', '', $tv);
$details[$tk] = null;
} else if (strpos($tv, 'b/s') !== false) {
$rfi['bitrate'] = str_replace(array('b/s', ' '), '', $tv);
$details[$tk] = null;
} else {
$details[$tk] = null;
$details[strval($tk)] = str_replace('/', '', $tv);
}
}
}
// Convert kb/s to b/s
foreach ($rfi as $k => $v) {
if (preg_match("/^[\d\.]+[\s]*k$/", $v)) {
$rfi[$k] = 1000 * doubleval(str_replace('k', '', $v));
}
}
// Remove
// PHP5.3: $details = array_filter($details, function($v) { return $v !== null; });
$tmp = array();
foreach ($details as $k => $v) {
if (!is_null($v)) {
$tmp[$k] = $v;
}
}
$details = $tmp;
unset($tmp);
if (!empty($details)) {
$rfi['details'] = $details;
}
} else if (strpos($r[0], 'chapter') === 0) {
// Chapter format like this:
// Chapter #0.0: -0.080000, end 505.440000
// Metadata:
// title : Chapter 2
// Chapter #0.1: start 505.440000, end 807.840000
// Metadata:
// title : Chapter 2
$k = count($fi['chapters']);
$fi['chapters'][$k] = array();
$rfi = &$fi['chapters'][$k];
// Chapter is like
$rfi['chapter'] = strtolower(trim(str_replace(array('chapter', '#0.'), '', $r[0])));
$r[1] = explode(',', strtolower(str_replace(array('start', 'end'), '', $r[1])));
$rfi['start'] = trim($r[1][0]);
$rfi['end'] = trim($r[1][1]);
} else if (strpos($r[0], 'metadata') === 0) {
// Metadata of the stream
if (!isset($rfi)) {
throw new FfmpegException("Failed to read file info: Metadata without referred stream or chapter");
}
$rfi['metadata'] = array();
while (!empty($return) && strlen(reset($return)) - strlen(ltrim(reset($return))) > $indent2) {
$r = explode(':', array_shift($return), 2);
$rfi['metadata'][trim(reset($r))] = trim(end($r));
}
} else {
$returnRemaining[] = $r;
}
}
}
// Determine additional information for getters
$fi['stream-count'] = array('video' => 0, 'audio' => 0, 'data' => 0);
foreach ($fi['streams'] as $v) {
$fi['stream-count'][$v['type']] = isset($fi['stream-count'][$v['type']]) ? $fi['stream-count'][$v['type']] + 1 : 1;
}
$fi['stream-index'] = array('video' => array(), 'audio' => array(), 'data' => array());
for ($i = count($fi['streams']) - 1; $i >= 0; $i--) {
if (!isset($fi['stream-index'][$fi['streams'][$i]['type']])) {
$fi['stream-index'][$fi['streams'][$i]['type']] = array();
}
$fi['stream-index'][$fi['streams'][$i]['type']][] = $i;
}
// Top level data
if (isset($fi['duration']) && !is_numeric($fi['duration'])) {
if (preg_match('/^[\d]+:[\d]+:[\d]+[\.\d]+$/i', $fi['duration'])) {
$v = explode(':', $fi['duration']);
$fi['duration'] = 3600 * intval($v[0]) + 60 * intval($v[1]) + doubleval($v[2]);
}
}
if (isset($fi['bitrate']) && strpos($fi['bitrate'], 'b/s') !== false) {
$fi['bitrate'] = str_replace(array(' ', 'b/s'), '', $fi['bitrate']);
if (strpos($fi['bitrate'], 'k') !== false) {
$fi['bitrate'] = 1000 * str_replace('k', '', $fi['bitrate']);
}
}
} catch(\Exception $e) {
$theExeption = $e;
}
$trc = array(
'full-stdout' => $stdout,
'recognized' => $fi,
'remaining-stdout' => array_merge($returnRemaining, $return),
);
Tracer::trace_r($trc, 'return', 3);
if(isset($theExeption)) {
throw $theExeption;
}
return $fi;
}
/**
* Internal onStdOut/onStdErr callback for convertFile()
* @param string $text
* @return bool
*/
public function convertFileCallback(&$text) {
if (stripos($text, 'frame=') !== false) {
$return = $progress = array();
foreach (explode("\n", $text) as $t) {
if (stripos($t, 'frame=') !== false && stripos($t, 'time=') !== false && stripos($t, 'bitrate=') !== false) {
$progress = array_pop(explode("\r", trim($t, " \t\r")));
} else {
$return[] = $t;
}
}
$text = implode("\n", $return);
$progress = explode(":", trim(array_shift(explode("bitrate=", array_pop(explode('time=', $progress))))));
$progress = 3600 * $progress[0] + 60 * $progress[1] + $progress[2];
if ($this->getDurationSeconds() > 0) {
$progress /= $this->getDurationSeconds();
} else {
$progress += 0.1; // just say something happens ...
}
if ($progress > 1) {
$progress = 1.0;
}
if ($this->progressCallback != null) {
call_user_func($this->progressCallback, $progress);
}
}
return true;
}
/**
* File convertion/transcoding
* @param string $inFile
* @param string $outFile
* @param array $args
*/
public static function convertFile($inFile, $outFile = null, $formula = null, $args = array(), $callback = null, array $metadata = array()) {
// Conversion formula check
if (empty($formula)) {
$formula = self::$config['default-formula'];
}
if (empty($args)) {
$args = array();
}
Tracer::trace("convertFile(\$inFile=$inFile, \$outFile=$outFile, \$formula=$formula, \$args=" . print_r($args, true) . ")", 1);
$class = str_replace('-', '_', "ffmpeg-$formula");
if (!class_exists($class, false)) {
if (!is_file(self::$config['formula-dir'] . "/ffmpeg-$formula.php")) {
throw new FfmpegException("No such transcoding formula: :formula", array(':formula' => $formula));
} else if (!@include_once(self::$config['formula-dir'] . "/ffmpeg-$formula.php")) {
throw new FfmpegException("Failed to include :formula", array(':formula' => "ffmpeg-$formula"));
} else {
if (!class_exists($class)) {
throw new FfmpegException("Class '$class' does not exist after including ':formula'", array(':formula' => $formula));
}
}
}
// Input file check && file info
if (!is_file($inFile)) {
throw new FfmpegException("Failed to transcode, input file does not exist: :infile", array(':infile' => $inFile));
} else {
$inFileInfo = self::readFileInfo($inFile);
}
// Create formula
$formula = new $class($inFileInfo, $args, self::getSupportedFormats());
if (empty($args)) {
$args = array();
} else if (!is_array($args)) {
throw new FfmpegException("Failed to transcode, additional args must be an array");
}
// Get conversion arguments, preserve positions of -i and -loglgevel
$args = array_merge(array(
//'-v' => 0,
array('-y' => null),
//'-xerror' => null,
array('-loglevel' => 'info'),
array('-i' => $inFile),
), $formula->getArguments());
foreach ($metadata as $k => $v) {
if (!is_null($v)) {
$args[] = array('-metadata' => "$k=$v");
}
}
if (empty($outFile)) {
if (strpos($inFile, '.') !== false) {
// remove extension
$outFile = explode('.', $inFile);
array_pop($outFile);
$outFile = implode('.', $outFile);
}
$outFileExt = trim($formula->getOutputFileExtension(), ' .');
if ($outFileExt == '') {
$outFileExt = FileSystem::getExtension($inFile);
}
$outFile .= '.' . $outFileExt;
if ($outFile == $inFile) {
$outFile = $inFile . '.out.' . FileSystem::getExtension($inFile);
}
}
if (file_exists($outFile)) {
throw new FfmpegException("Failed to transcode, output file already exist: :outfile", array(':$outfile' => $outFile));
}
return self::executeFfmpeg($args, $outFile, $callback);
}
/**
* Changes metadata by array or callback. Note: Ffmpeg needs the same disk
* space for an intermediate file as the input file is big. After conversion
* with stream copy and new metadata the input file will be overwritten with
* the new file.
* @param string $file
* @param mixed $edits
* @param array $callback_args=null
* @param bool $simulate=false
* @return array
*/
public static function changeFileMetadata($file, $edits, $callback_args = null, $simulate=false) {
if (empty($edits))
return;
if (!is_file($file)) {
throw new FfmpegException('Input file does not exist: "' . $file . '"');
} else if (!is_readable($file)) {
throw new FfmpegException('Input file is not readable: "' . $file . '"');
}
$meta = self::readFileInfo($file);
$cp_meta = $meta = $meta['meta'];
if (is_callable($edits)) {
if (empty($callback_args)) {
$callback_args = array();
} else if(!is_array($callback_args)) {
throw new FfmpegException('If you specify callback arguments, this must be given as array. (processed file="' . $file . '"');
}
$callback_args['file'] = realpath($file);
$meta = $edits($meta, $callback_args);
if ($meta === false) {
return;
} else if (!is_array($meta)) {
throw new FfmpegException('Metadata editing callback must return an array or FALSE.');
}
} else if (is_array($edits)) {
foreach ($edits as $k => $v) {
if (gettype($k) != 'string') {
throw new FfmpegException('The meta key type not valid: "' . $k . '"');
} else if (trim($k) == '' || preg_replace('/[\w\d_]/', '', $k) !== '') {
throw new FfmpegException('The meta key type contains non-word characters: "' . $k . '"');
} else if (!is_scalar($v)) {
throw new FfmpegException('The meta value type is nonscalar: "' . $k . '" type = "' . gettype($v) . '"');
} else {
$meta[$k] = trim($v);
}
}
} else {
throw new FfmpegException('Metadata edits is no array and not callable.');
}
// Data that should better not be changed (and ffmpeg will not do it anyway),
// means the simulation matches the reality a little bit better ...
if (isset($cp_meta['major_brand']))
$meta['major_brand'] = $cp_meta['major_brand'];
if (isset($cp_meta['minor_version']))
$meta['minor_version'] = $cp_meta['minor_version'];
if (isset($cp_meta['compatible_brands']))
$meta['compatible_brands'] = $cp_meta['compatible_brands'];
if (isset($cp_meta['creation_time']))
$meta['creation_time'] = $cp_meta['creation_time'];
if (isset($cp_meta['encoder']))
$meta['encoder'] = $cp_meta['encoder'];
if(empty($simulate)) {
// Set data
try {
$outFile = FileSystem::getTempFileName() . '.' . FileSystem::getExtension($file);
Tracer::trace("INFILE=$file, OUTFILE=$outFile");
Tracer::trace_r($meta, 'New metadata to set');
self::convertFile($file, $outFile, 'copy', array(), null, $meta);
FileSystem::move($outFile, $file);
} catch (\Exception $e) {
if (is_file($outFile))
@unlink($outFile);
throw $e;
}
}
return $meta;
}
/**
* Constructor
* @param string $filePath
* @param mixed $progressCallback
*/
public function __construct($filePath = null, $progressCallback = null) {
self::config();
$this->setPath($filePath);
$this->setProgressCallback($progressCallback);
}
/**
* Returns the file path
* @return string
*/
public function getPath() {
return $this->filePath;
}
/**
* Set the path of the file, automatically reads the file info
* @param string $filePath
*/
public function setPath($filePath) {
$this->filePath = empty($filePath) ? '' : $filePath;
$this->fileInfo = array();
$this->fileInfo = self::readFileInfo($filePath);
}
/**
* Sets the progress callback. The function/method pattern has to be as
* function(int $progress) { ... }
* @param mixed $callback
*/
public function setProgressCallback($callback) {
if (empty($callback)) {
$this->progressCallback = null;
} else if (!is_callable($callback)) {
throw new FfmpegException('Specified progress callback is not callable.');
} else {
$this->progressCallback = $callback;
}
}
/**
* Returns the process callback
* @return mixed
*/
public function getProgressCallback() {
return $this->progressCallback;
}
/**
* Returns the complete file info array
* @return array
*/
public function getFileInfo() {
return $this->fileInfo;
}
/**
* Returns the container type
* @return string
*/
public function getContainerFormat() {
return $this->fileInfo['format'];
}
/**
* Returns the container metadata as array, or the value of a particular key.
* @param string $key
* @return mixed
*/
public function getMetadata($key = null) {
return $this->getContainerMetaData($key);
}
/**
* Returns the container metadata as array, or the value of a particular key.
* @param string $key
* @return mixed
*/
public function getContainerMetaData($key = null) {
if (empty($key)) {
return $this->fileInfo['meta'];
} else if (isset($this->fileInfo['meta'][$key])) {
return $this->fileInfo['meta'][$key];
} else {
$v = array_change_key_case($this->fileInfo['meta'], CASE_LOWER);
$key = strtolower($key);
if (isset($v[$key])) {
return $v[$key];
} else {
throw new FfmpegException("Metadata not found for key ':key'", array(':key' => $key));
}
}
}
/**
* Returns the duration in format: hh:mm:ss, seconds are rounded
* @return string
*/
public function getDuration() {
if (!isset($this->fileInfo['duration'])) {
return '00:00:00.0';
}
$t = doubleval($this->fileInfo['duration']);
$h = intval($t / 3600);
$t -= 3600 * $h;
$m = intval($t / 60);
$t -= 60 * $m;
$t = round($t);
return sprintf("%02d:%02d:%02d", $h, $m, $t);
}
/**
* Returns the double/float value of the duration in seconds
* @return double
*/
public function getDurationSeconds() {
return isset($this->fileInfo['duration']) ? $this->fileInfo['duration'] : 0;
}
/**
* Returns the number of video streams
* @return int
*/
public function getVideoStreamCount() {
return $this->fileInfo['stream-count']['video'];
}
/**
* Returns if the file contains video information
* @return bool
*/
public function hasVideo() {
return $this->fileInfo['stream-count']['video'] > 0;
}
/**
* Returns the information about the video stream as assoc. array. For the
* case that there are more video streams with index
* @param int $index
* @return array
*/
public function getVideoStreamInfo($index = 0) {
if (empty($this->fileInfo['stream-index']['video'])) {
throw new FfmpegException("File has no video stream");
} else if (!isset($this->fileInfo['stream-index']['video'][$index])) {
throw new FfmpegException("File has no video stream with index :index", array(':index' => $index));
}
return $this->fileInfo['streams'][$this->fileInfo['stream-index']['video'][$index]];
}
/**
* Returns the video codec
* @return string
*/
public function getVideoCodec() {
$v = $this->getVideoStreamInfo();
return $v['codec'];
}
/**
* Returns the video bit rate
* @return double
*/
public function getVideoBitrate() {
$v = $this->getVideoStreamInfo();
return $v['bitrate'];
}
/**
* Returns the video width
* @return int
*/
public function getVideoWidth() {
$v = $this->getVideoStreamInfo();
return $v['width'];
}
/**
* Returns the video height
* @return int
*/
public function getVideoHeight() {
$v = $this->getVideoStreamInfo();
return $v['height'];
}
/**
* Returns the video frames per second
* @return double
*/
public function getVideoFramesPerSecond() {
$v = $this->getVideoStreamInfo();
return $v['fps'];
}
/**
* Returns the number of audio streams
* @return int
*/
public function getAudioStreamCount() {
return $this->fileInfo['stream-count']['audio'];
}
/**
* Returns the video pixel format
* @return string
*/
public function getVideoPixelFormat() {
return isset($this->fileInfo['pixelformat']) ? $this->fileInfo['pixelformat'] : 0;
}
/**
* Returns if the file contains audio information
* @return bool
*/
public function hasAudio() {
return $this->fileInfo['stream-count']['audio'] > 0;
}
/**
* Returns the information about the audio stream as assoc. array
* @param int $index
* @return array
*/
public function getAudioStreamInfo($index = 0) {
if (empty($this->fileInfo['stream-index']['audio'])) {
throw new FfmpegException("File has no audio stream");
} else if (!isset($this->fileInfo['stream-index']['audio'][$index])) {
throw new FfmpegException("File has no audio stream with index :index", array(':index' => $index));
}
return $this->fileInfo['streams'][$this->fileInfo['stream-index']['audio'][$index]];
}
/**
* Returns the audio codec
* @param int $index
* @return string
*/
public function getAudioCodec($index = 0) {
$v = $this->getAudioStreamInfo($index);
return $v['codec'];
}
/**
* Returns the audio bit rate in bits per second (e.g. 128000)
* @param int $index
* @return int
*/
public function getAudioBitrate($index = 0) {
$v = $this->getAudioStreamInfo($index);
return $v['bitrate'];
}
/**
* Returns the audio sample rate (e.g. 44000)
* @param int $index
* @return int
*/
public function getAudioSampleRate($index = 0) {
$v = $this->getAudioStreamInfo($index);
return $v['sample-rate'];
}
/**
* Returns the audio sample bits (e.g. 16 bit)
* @param int $index
* @return int
*/
public function getAudioSampleBits($index = 0) {
$v = $this->getAudioStreamInfo($index);
return $v['sample-bits'];
}
/**
* Returns the number of audio channels (e.g. 2 for stereo)
* @param int $index
* @return int
*/
public function getAudioChannelCount($index = 0) {
$v = $this->getAudioStreamInfo($index);
return $v['channels'];
}
/**
* Retuens if an audio stream is mono
* @param int $index
* @return bool
*/
public function isAudioMono($index = 0) {
return $this->getAudioChannelCount($index) == 1;
}
/**
* Retuens if an audio stream is stereo
* @param int $index
* @return bool
*/
public function isAudioStereo($index = 0) {
return $this->getAudioChannelCount($index) == 2;
}
/**
* Returns the number of audio streams
* @return int
*/
public function getDataStreamCount() {
return $this->fileInfo['stream-count']['data'];
}
/**
* Returns if the file contains subtitles information
* @return bool
*/
public function hasData() {
return $this->fileInfo['stream-count']['data'] > 0;
}
/**
* Returns the information about the audio stream
* @param int $index
*/
public function getDataStreamInfo($index = 0) {
if (!is_numeric($index) || $index < 0) {
throw new FfmpegException("Index of a stream info must be an integer >= 0 (given=':index')", array(':index' => $index));
}
foreach ($this->fileInfo['streams'] as $v) {
if ($v['type'] == 'data' && ($index--) <= 0) {
return $v;
}
}
throw new FfmpegException("No such audio stream with index :index", array(':index' => $index));
}
/**
* File convertion/transcoding
* @param string $outFile
* @param string $formula
* @param array $args
* @param array $metadata
*/
public function convert($outFile = null, $formula = null, $args = array(), array $metadata = array()) {
self::convertFile($this->filePath, $outFile, $formula, $args, array($this, 'convertFileCallback'), $metadata);
}
}