GIT repositories

Index page of all the GIT repositories that are clonable form this server via HTTPS. Übersichtsseite aller GIT-Repositories, die von diesem Server aus über git clone (HTTPS) erreichbar sind.

Services

A bunch of service scripts to convert, analyse and generate data. Ein paar Services zum Konvertieren, Analysieren und Generieren von Daten.

GNU octave web interface

A web interface for GNU Octave, which allows to run scientific calculations from netbooks, tables or smartphones. The interface provides a web form generator for Octave script parameters with pre-validation, automatic script list generation, as well presenting of output text, figures and files in a output HTML page. Ein Webinterface für GNU-Octave, mit dem wissenschaftliche Berechnungen von Netbooks, Tablets oder Smartphones aus durchgeführt werden können. Die Schnittstelle beinhaltet einen Formulargenerator für Octave-Scriptparameter, mit Einheiten und Einfabevalidierung. Textausgabe, Abbildungen und generierte Dateien werden abgefangen und in einer HTML-Seite dem Nutzer als Ergebnis zur Verfügung gestellt.

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);
  }
 
}