Login   Register  
PHP Classes
elePHPant
Icontem

File: src/PHPVideoToolkit/ExecBuffer.php

Recommend this page to a friend!
Stumble It! Stumble It! Bookmark in del.icio.us Bookmark in del.icio.us
  Classes of Oliver Lillie  >  PHP Video Toolkit  >  src/PHPVideoToolkit/ExecBuffer.php  >  Download  
File: src/PHPVideoToolkit/ExecBuffer.php
Role: Class source
Content type: text/plain
Description: Class source
Class: PHP Video Toolkit
Manipulate and convert videos with ffmpeg program
Author: By
Last change: Replaced _run recursive call with a while loop.
Provides exactly the same functionality with the benefits of no longer gradually increasing memory usage of the PHP process while transcoding.
No longer trips up on xdebug.max_nesting_level setting.
Date: 4 months ago
Size: 21,603 bytes
 

Contents

Class file image Download
<?php
    
    /**
     * This file is part of the PHP Video Toolkit v2 package.
     *
     * @author Oliver Lillie (aka buggedcom) <publicmail@buggedcom.co.uk>
     * @license Dual licensed under MIT and GPLv2
     * @copyright Copyright (c) 2008-2014 Oliver Lillie <http://www.buggedcom.co.uk>
     * @package PHPVideoToolkit V2
     * @version 2.1.1
     * @uses ffmpeg http://ffmpeg.sourceforge.net/
     */
     
    namespace PHPVideoToolkit;
     
    /**
     * undocumented class
     *
     * @access public
     * @author Oliver Lillie
     * @package default
     */
    class ExecBuffer //extends Loggable
    {
        protected $_failure_tracking;
        protected $_blocking;
        protected $_output;
        protected $_temp_directory;
        
        protected $_executed_command;
        protected $_command;
        protected $_buffer;
        protected $_error_code;
            
        protected $_running;
        protected $_start_time;
        protected $_end_time;
        protected $_callback_period_interval;
        
        protected $_boundary;
        protected $_failure_boundary;
        protected $_completion_boundary;
        protected $_error_code_boundary;
        
        protected $_php_exec_infinite_timelimit;
        
        protected $_tmp_files;
        
        const DEV_NULL = '/dev/null';
        const TEMP = -1;

        protected static $_is_windows = null;
        
        public function __construct($exec_command_string, $temp_directory=null, $php_exec_infinite_timelimit=true)
        {
            if(self::$_is_windows === null)
            {
                self::$_is_windows = strtoupper(substr(PHP_OS, 0, 3)) === 'WIN';
            }

            $this->setTempDirectory($temp_directory);
            
            $this->_failure_tracking = true;
            $this->_blocking = true;
            $this->_output = self::TEMP;
            $this->_buffer = null;
            $this->_error_code = null;
            
            $this->_running = false;
            $this->_start_time = null;
            $this->_end_time = null;
            $this->_callback_period_interval = 1;
            
            $this->_php_exec_infinite_timelimit = $php_exec_infinite_timelimit;
            
            $this->_tmp_files = array();
            
            $id = uniqid(rand(99999, 999999).'-').'-'.md5(__FILE__);
            $this->_boundary = $id;
            $this->_error_code_boundary = '<e-'.$id.'>';
            $this->_failure_boundary = '<f-'.$id.'>';
            $this->_completion_boundary = '<c-'.$id.'>';
            
            $this->_command = $exec_command_string;
        }
        
        public function setTempDirectory($temp_directory=null)
        {
            if($temp_directory === null)
            {
                $temp_directory = sys_get_temp_dir();
            }
            if(is_dir($temp_directory) === false)
            {
                throw new Exception('The temp directory does not exist or is not a directory.');
            }
            else if(is_readable($temp_directory) === false)
            {
                throw new Exception('The temp directory is not readable.');
            }
            else if(is_writable($temp_directory) === false)
            {
                throw new Exception('The temp directory is not writeable.');
            }
            $this->_temp_directory = $temp_directory;
            
            return $this;
        }
        
        /**
         * Destruct function autoamtically tidies up tmp files.
         *
         * @access public
         * @author Oliver Lillie
         * @return void
         */
        public function __destruct()
        {
            if(empty($this->_tmp_files) === false)
            {
                foreach ($this->_tmp_files as $path)
                {
                    //@unlink($path);
                }
            }
        }
        
        /**
         * Start the exec, essentially call exec().
         *
         * @access public
         * @author Oliver Lillie
         * @return void
         */
        public function execute($callback=null)
        {
//          check the the callback is callable.
            if($callback !== null)
            {
                if(is_callable($callback) === false)
                {
                    throw new Exception('The supplied callback is not callable.');
                }
                $this->setBlocking(true);
            }
            
//          get the execution string
            $this->_executed_command = $this->getExecString();

//          prevent the script timing out if the configuration allows.
            $previous_time_limit = ini_get('max_execution_time');
            if($this->_php_exec_infinite_timelimit === true)
            {
                set_time_limit(0);
            }

//          do the execution.
            $this->_running = true;
            $this->_start_time = time()+microtime();
            exec($this->_executed_command, $buffer, $err); // note error cannot be replied upon because of the output options.
            $buffer = implode(PHP_EOL, $buffer);
            $this->_error_code = $err;
            
//          reset the timelimit if required
            if($previous_time_limit > 0 && $this->_php_exec_infinite_timelimit === true)
            {
                set_time_limit($previous_time_limit);
            }
            
//          do we need to process output buffer
            if(empty($this->_output) === false)
            {
//              if the process is blocking, the run
                if($this->_blocking === true)
                {
                    if($callback !== null && is_callable($callback) === true)
                    {
                        call_user_func($callback, $this, null, null);
                    }
                    $this->_run($callback);
                }
            }
            
            if($this->_blocking === true)
            {
                $this->_end_time = time()+microtime();
            }
            
//          this is the final callback
            if($callback !== null && is_callable($callback) === true)
            {
                call_user_func($callback, $this, null, true);
            }
            
            return $this;
        }
        
        /**
         * Gets the current run time of the command execution.
         *
         * @access public
         * @author Oliver Lillie
         * @return void
         */
        public function getRunTime()
        {
            if(empty($this->_start_time) === true)
            {
                throw new Exception('Unable to read runtime as command has not yet been executed.');
            }
            if(empty($this->_end_time) === false)
            {
                $end = $this->_end_time;
            }
            else
            {
                $end = time()+microtime();
            }
            
            return $end - $this->_start_time;
        }
        
        /**
         * Runs the read/wait loop.
         *
         * @access public
         * @author Oliver Lillie
         * @param mixed $callback 
         * @return void
         */
        protected function _run($callback)
        {
            while($this->_running !== false) {
//              get the buffer regardless of wether or not there is a callback as it updates and 
//              checks for the completion of the command.
                $buffer = $this->getBuffer();

//              get the buffer to give to the
                if($callback !== null && is_callable($callback) === true)
                {
                    call_user_func($callback, $this, $buffer, false);
                }

//              if we have finished running the loop then break here.
                if($this->_running === false)
                {
                    break;
                }

//              still running so wait and then run again.
                $this->wait($this->_callback_period_interval);
            }
        }
        
        /**
         * Makes the read/wait loop wait.
         *
         * @access public
         * @author Oliver Lillie
         * @return void
         */
        public function wait($seconds=1)
        {
            usleep($seconds*100000);
        }
        
        /**
         * Stops the read/wait loop from running.
         *
         * @access public
         * @author Oliver Lillie
         * @return void
         */
        public function stop()
        {
            $this->_running = false;
        }
        
        /**
         * Returns the buffer.
         *
         * @access public
         * @author Oliver Lillie
         * @return string
         */
        public function getRawBuffer()
        {
            if(empty($this->_output) === false)
            {
                $this->_buffer = file_get_contents($this->_output);
                $this->_detectCompletionAndEnd();
            }
            
            return $this->_buffer;
        }
        
        /**
         * Returns the buffer.
         *
         * @access public
         * @author Oliver Lillie
         * @return string
         */
        public function getBuffer()
        {
            return rtrim(preg_replace(array('/'.$this->_failure_boundary.'/', '/'.$this->_completion_boundary.'/', '/'.$this->_error_code_boundary.(self::$_is_windows === false ? '([0-9]+)' : '').'/'), '', $this->getRawBuffer()));
        }
        
        /**
         * Returns the buffer.
         *
         * @access public
         * @author Oliver Lillie
         * @return string
         */
        public function getLastLine()
        {
            $buffer = $this->getBuffer();
            $lines = preg_split('/\r\n|\r|\n/', $buffer);
            return array_pop($lines);
        }
        
        /**
         * Returns the buffer.
         *
         * @access public
         * @author Oliver Lillie
         * @return string
         */
        public function getLastSplit()
        {
            $buffer = $this->getBuffer();
            $splits = explode(PHP_EOL, $buffer);
            return array_pop($splits);
        }
        
        /**
         * Deletes the output buffer file.
         *
         * @access public
         * @author Oliver Lillie
         * @return void
         */
        public function deleteOutputFile()
        {
            if(empty($this->_output) === false && is_file($this->_output) === true)
            {
                @unlink($this->_output);
                $this->_output = null;
            }
        }
        
        /**
         * Detects the failure boundary in the output.
         *
         * @access protected
         * @author Oliver Lillie
         * @param boolean $update_buffer 
         * @return boolean
         */
        protected function _detectFailureBoundaryInOutput($update_buffer=false)
        {
            if($this->_failure_tracking !== true)
            {
                throw new Exception('Failure tracking is not enabled. Unable to determine if command failed.');
            }
            
//          do we need to update the buffer?
            if($update_buffer === true)
            {
                $this->getBuffer();
            }
            
            return strpos($this->_buffer, $this->_failure_boundary) !== false;
        }
        
        public function getErrorCode()
        {
            return $this->_getErrorCodeInOutput();
        }
        
        public function hasError()
        {
            return $this->_detectFailureBoundaryInOutput(true);
        }
        
        public function isCompleted()
        {
            return $this->_detectCompletionBoundaryInOutput(true);
        }
        
        protected function _detectCompletionAndEnd()
        {
//          detect if the output has completed, and if it does
//          delete the output file and stop the loop.
            if($this->_detectCompletionBoundaryInOutput() === true)
            {
                $this->deleteOutputFile();
                $this->stop();
            }
            else if($this->_detectFailureBoundaryInOutput() === true)
            {
                $this->_error_code = $this->_getErrorCodeInOutput();
            }
        }
        
        protected function _getErrorCodeInOutput($update_buffer=false)
        {
//          do we need to update the buffer?
            if($update_buffer === true)
            {
                $this->getBuffer();
            }
            
            if(preg_match('/'.$this->_error_code_boundary.'([0-9]+)/', $this->_buffer, $matches) > 0)
            {
                return $matches[1];
            }
            return null;
        }
                
        
        /**
         * Detects the completion boundary in the output.
         *
         * @access protected
         * @author Oliver Lillie
         * @param boolean $update_buffer 
         * @return boolean
         */
        protected function _detectCompletionBoundaryInOutput($update_buffer=false)
        {
//          do we need to update the buffer?
            if($update_buffer === true)
            {
                $this->getBuffer();
            }
            
            return strpos($this->_buffer, $this->_completion_boundary) !== false;
        }
        
        /**
         * Returns an augmented command string based on the classes options.
         *
         * @access public
         * @author Oliver Lillie
         * @return string
         */
        public function getExecString()
        {
            $command_string = $this->_command;
            
//          get the output append string
            $output = $this->getBufferOutput();
            if(empty($output) === false)
            {
                $output = ' > '.escapeshellarg($output);
            }
            
//          get the buffer divert string.
            $buffer_divert = '';
            if(empty($output) === false || empty($tracking) === false)
            {
                $buffer_divert = ' 2>&1 &';
            }
            
//          get the failure tracking boundaries
//          and get the completion boundary
            $completion_boundary_open = '';
            $completion_boundary_close = '';
            if($this->_failure_tracking === true)
            {
                $completion_boundary_open = '((';
                $completion_boundary_close = ' && echo '.$this->_escapeBoundaryInEcho($this->_completion_boundary).') || echo '.$this->_escapeBoundaryInEcho($this->_failure_boundary).' '.$this->_escapeBoundaryInEcho($this->_completion_boundary).' '.$this->_escapeBoundaryInEcho($this->_error_code_boundary).(self::$_is_windows === false ? '$?' : '').') 2>&1';
            }
            else
            {
                $completion_boundary_open = '(';
                $completion_boundary_close = ' && echo '.$this->_escapeBoundaryInEcho($this->_completion_boundary).') 2>&1';
            }
            
//          compile the final command and track
            return $completion_boundary_open.$command_string.$completion_boundary_close.$output.$buffer_divert;
        }

        /**
         * Escapes the boundaries, based on OS.
         *
         * @access public
         * @author Oliver Lillie
         * @param string $string 
         * @return string
         */
        protected function _escapeBoundaryInEcho($boundary)
        {
            return self::$_is_windows === true ? str_replace(array('<', '>'), array('^<', '^>'), $boundary) : escapeshellarg($boundary);
        }
        
        /**
         * Returns the executred command.
         *
         * @access public
         * @author Oliver Lillie
         * @return string
         */
        public function getExecutedCommand()
        {
            if(empty($this->_executed_command) === true)
            {
                throw new Exception('The command has not yet been executed.');
            }
            
            return $this->_executed_command;
        }
        
        /**
         * Returns the original command.
         *
         * @access public
         * @author Oliver Lillie
         * @return string
         */
        public function getCommand()
        {
            return $this->_command;
        }
        
        /**
         * Sets the pid tracking status of the query.
         *
         * @access public
         * @author Oliver Lillie
         * @param string $enable_pid_tracking 
         * @return void
         */
        public function setFailureTracking($enable_failure_tracking)
        {
            if(is_bool($enable_failure_tracking) === false && is_null($enable_failure_tracking) === false)
            {
                throw new Exception('$enable_failure_tracking must be a boolean (or null) value.');
            }
            
            $this->_failure_tracking = $enable_failure_tracking;
            return $this;
        }
        
        /**
         * Gets the failure tracking status of the exec command.
         *
         * @access public
         * @author Oliver Lillie
         * @return mixed
         */
        public function getFailureTracking()
        {
            return $this->_failure_tracking;
        }
        
        /**
         * Sets the callback period wait interval.
         *
         * @access public
         * @author Oliver Lillie
         * @param integer $callback_period_interval
         * @return void
         */
        public function setCallbackWaitInterval($callback_period_interval)
        {
            if(is_int($pid) === false)
            {
                throw new Exception('$callback_period_interval must be an integer.');
            }
            
            $this->_callback_period_interval = $callback_period_interval;
            return $this;
        }
        
        /**
         * Gets the callback period wait interval.
         *
         * @access public
         * @author Oliver Lillie
         * @return mixed
         */
        public function getCallbackWaitInterval()
        {
            return $this->_callback_period_interval;
        }
        
        /**
         * Sets the exec calls blocking mode.
         *
         * @access public
         * @author Oliver Lillie
         * @param boolean $enable_blocking 
         * @return void
         */
        public function setBlocking($enable_blocking)
        {
            if(is_bool($enable_blocking) === false && is_null($enable_blocking) === false)
            {
                throw new Exception('$enable_blocking must be a boolean (or null) value.');
            }
            
            $this->_blocking = $enable_blocking;
            return $this;
        }
        
        /**
         * Returns the exec calls blocking mode.
         *
         * @access public
         * @author Oliver Lillie
         * @return boolean
         */
        public function getBlocking()
        {
            return $this->_blocking;
        }
        
        /**
         * Returns the boundary placeholder for this exec call.
         *
         * @access public
         * @author Oliver Lillie
         * @return string
         */
        public function getBoundary()
        {
            return $this->_boundary;
        }
        
        /**
         * Sets the output of the exec call.
         *
         * @access public
         * @author Oliver Lillie
         * @param mixed $enable_pid_tracking Can be one of the following constants. ExecBuffer::DEV_NULL, ExecBuffer::TEMP, 
         *  a string will be interpretted as a file or null will output everything to sdout.
         * @return void
         */
        public function setBufferOutput($buffer_output)
        {
            if(in_array($buffer_output, array(null, self::DEV_NULL, self::TEMP)) === false)
            {
                $dir = dirname($buffer_ouput);
                if(is_dir($dir) === false)
                {
                    throw new Exception('Buffer output parent directory "'.$dir.'" is not a directory.');
                }
                else if(is_readable($dir) === false || is_writeable($dir) === false)
                {
                    throw new Exception('Buffer output parent directory "'.$dir.'" is not read-writable by the webserver.');
                }
            }
            
            $this->_output = $buffer_output;
            return $this;
        }
        
        /**
         * Gets the status of the buffer output.
         *
         * @access public
         * @author Oliver Lillie
         * @return string
         */
        public function getBufferOutput()
        {
            if($this->_output === null)
            {
                if($this->_blocking === false)
                {
                    return $this->_output = $this->generateTmpFile();
                }
            }
            else if($this->_output === self::TEMP)
            {
                return $this->_output = $this->generateTmpFile();
            }
            
            return $this->_output;
        }
        
        /**
         * Generate and create a temp file.
         *
         * @access public
         * @author Oliver Lillie
         * @return string
         */
        public function generateTmpFile($prefix='')
        {
            $tmp = tempnam($this->_temp_directory, 'phpvideotoolkit_'.$prefix);
            array_push($this->_tmp_files, $tmp);
            return $tmp;            
        }
        
    }