PHP Classes

File: src/PHPVideoToolkit/ExecBuffer.php

Recommend this page to a friend!
  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: 8 years 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; } }