PHP Classes

File: src/PHPVideoToolkit/Media.php

Recommend this page to a friend!
  Classes of Oliver Lillie   PHP Video Toolkit   src/PHPVideoToolkit/Media.php   Download  
File: src/PHPVideoToolkit/Media.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: More renames
Read Volume Component

Added function readVolumeComponent() to read volume component.
added various input format fixes
fix for #41

Signed-off-by: Oliver Lillie <buggedcom@gmail.com>
#40 add remove command functions to remove commands from the process

Signed-off-by: Oliver Lillie <buggedcom@gmail.com>
updated exception types thrown

Signed-off-by: Oliver Lillie <buggedcom@gmail.com>
fixes constant typeo
updated php doc bloc @author so not to contain :
updated format constructors so that the default input/output type is output. removed 'output' from the constructors in readme
fixed issues with portable progress handler
Merge branch 'refs/heads/multiple-output'

Conflicts:
README.md
src/PHPVideoToolkit/ProgressHandlerOutput.php
src/PHPVideoToolkit/ProgressHandlerPortable.php
bug fix for incorrectly detecting if a format exists
updated version in source
fixed an issue where the output if using %timecode or %index would not be automatically renamed unless ->getOutput was called. #24 - Thanks @cassianotartari for aiding in discovering this bug.
applied default Media::OVERWRITE_XXX functionality to animated gifs
added support for setting 1/n frame rates. ref #25
corrected issue where the incorrect output fps value was being used to construct the timecode output value
Date: 1 year ago
Size: 57,631 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.7-beta * @uses ffmpeg http://ffmpeg.sourceforge.net/ */ namespace PHPVideoToolkit; /** * This class provides generic data parsing for the output from FFmpeg from specific * media files. Parts of the code borrow heavily from Jorrit Schippers version of * PHPVideoToolkit v 0.1.9. * * @access public * @author Oliver Lillie * @author Jorrit Schippers * @package default */ class Media extends MediaParser { /** * Overwrite constants used in save, saveNonBlocking and getExecutionCommand */ const OVERWRITE_FAIL = -1; const OVERWRITE_EXISTING = -2; const OVERWRITE_UNIQUE = -3; protected $_media_file_path; protected $_media_input_format; private $_blocking; public $last_error_message; public $error_messages; protected $_extract_segment; protected $_split_options; protected $_metadata; protected $_supported_meta_data; private $_output_path; private $_processing_path; private $_post_process_callbacks; protected $_require_d_in_output; protected $_process; protected $_ignore_format; /** * Constructs a media object. * * @access public * @author Oliver Lillie * @param string $media_file_path The file path of a media file. * @param Config $config A PHPVideoToolkit Config object * @param Format $input_format An input Format object */ public function __construct($media_file_path, Config $config=null, Format $input_format=null) { parent::__construct($config, 'ffmpeg'); if($media_file_path !== null) { $this->setMediaPath($media_file_path); } $this->setInputFormat($input_format); $this->last_error_message = null; $this->error_messages = array(); $this->_extract_segment = array(); $this->_split_options = array(); $this->_metadata = array(); $this->_output_path = null; $this->_processing_path = null; $this->_blocking = null; $this->_require_d_in_output = false; $this->_ignore_format = false; // @see http://multimedia.cx/eggs/supplying-ffmpeg-with-metadata/ // @see http://wiki.multimedia.cx/index.php?title=FFmpeg_Metadata $this->_supported_meta_data = array( 'title', 'date', 'author', 'album_artist', 'album', 'grouping', 'composer', 'year', 'track', 'comment', 'genre', 'copyright', 'description', 'synopsis', 'show', 'episode_id', 'network', 'lyrics', ); $this->_post_process_callbacks = array(); $this->_process = new FfmpegProcessProgressable('ffmpeg', $this->_config); } protected function _validateMedia($media_type) { $type = $this->readType(); return $media_type === $type; } /** * Sets the input Format of the input file. * * @access public * @author Oliver Lillie * @param Format $input_format * @return self */ public function setInputFormat(Format $input_format=null) { // create a default input format if none is set. if($input_format === null) { $format = null; $ext = pathinfo($this->_media_file_path, PATHINFO_EXTENSION); if(empty($ext) === false) { // check we have a format we know about. $format = Extensions::toBestGuessFormat($ext); } $this->_media_input_format = $this->getDefaultFormat(Format::INPUT, $format); } else { $this->_media_input_format = $input_format; } return $this; } /** * Returns the input format. * * @access public * @author Oliver Lillie * @return Format */ public function getInputFormat() { return $this->_media_input_format; } public function getDefaultFormatClassName() { return 'Format'; } /** * Returns the default (empty) input format for the type of media object this class is. * * @access public * @author Oliver Lillie * @param string $type Either input for an input format or output for an output format. * @return Format */ public function getDefaultFormat($type, $format=null) { // $format is purposely ignored return $this->_getDefaultFormat($type, $this->getDefaultFormatClassName(), null); } /** * Returns a format class set to the specific output/input type. * * @access protected * @author Oliver Lillie * @param string $type Either input for an input format or output for an output format. * @param string $class_name The class name of the Format instance to return. * @package Format Returns an instance of a Format object or child class. */ protected function _getDefaultFormat($type, $default_class_name, $format) { // TODO replace with reference to Format::getFormatFor if(in_array($type, array(Format::OUTPUT, Format::INPUT)) === false) { throw new \InvalidArgumentException('Unrecognised format type "'.$type.'".'); } // check the requested class exists $class_name = '\\PHPVideoToolkit\\'.$default_class_name.(empty($format) === false ? '_'.ucfirst(strtolower($format)) : ''); if(class_exists($class_name) === false) { $requested_class_name = $class_name; $class_name = '\\PHPVideoToolkit\\'.$default_class_name; if(class_exists($class_name) === false) { throw new \InvalidArgumentException('Requested default format class does not exist, "'.($requested_class_name === $class_name ? $class_name : $requested_class_name.'" and "'.$class_name.'"').'".'); } } // check that it extends from the base Format class. if($class_name !== '\\PHPVideoToolkit\\Format' && is_subclass_of($class_name, '\\PHPVideoToolkit\\Format') === false) { throw new \InvalidArgumentException('The class "'.$class_name.'" is not a subclass of \\PHPVideoToolkit\\Format.'); } return new $class_name($type, $this->_config); } /** * Returns the real path of the media asset. * * @access public * @author Oliver Lillie * @return string */ public function getMediaPath() { return $this->_media_file_path; } /** * Returns the real path of the media asset. * * @access public * @author Oliver Lillie * @return self */ public function setMediaPath($media_file_path) { $real_file_path = realpath($media_file_path); if($real_file_path === false || is_file($real_file_path) === false) { throw new \InvalidArgumentException('The file "'.$media_file_path.'" cannot be found in \\PHPVideoToolkit\\Media::__construct.'); } else if(is_readable($real_file_path) === false) { throw new \InvalidArgumentException('The file "'.$media_file_path.'" is not readable in \\PHPVideoToolkit\\Media::__construct.'); } $this->_media_file_path = $real_file_path; return $this; } /** * Sets global meta data on the media. That being said "global" does not mean it sets the * meta data on the media streams, rather just the meta data on the container. * * @access public * @author Oliver Lillie * @param string $key * @param string $value * @param boolean $force * @return self */ public function setMetaData($key, $value=null, $force=false) { if(is_array($key) === true) { foreach ($key as $k => $v) { $this->setMetaData($k, $v); } return $this; } if(empty($key) === true) { throw new \InvalidArgumentException('Empty metadata key. Metadata keys must be at least one character long.'); } // check that meta key is supported by this format. $key = strtolower($key); if($force === false && in_array($key, $this->_supported_meta_data) === false) { throw new Exception('The metadata key "'.$key.'" cannot be set as it is not honoured by the muxer.'); } $this->_metadata[$key] = $value; return $this; } /** * Removes all the global meta data. That being said "global" does not mean it removes the * meta data from the media streams, rather just the meta data on the container. * * @access public * @author Oliver Lillie * @return self */ public function purgeMetaData() { $this->_metadata = array(); $meta = $this->readGlobalMetaData(); if(empty($meta) === false) { foreach ($meta as $key => $ignored) { $this->setMetaData($key, '', true); } } return $this; } /** * Extracts a segment of the media object. * * @access public * @author Oliver Lillie * @param Timecode $from_timecode * @param Timecode $to_timecode * @param boolean $accurate If true then accuracy is prefered over performance. * @return Media */ public function extractSegment(Timecode $from_timecode=null, Timecode $to_timecode=null, $accurate=false) { // check that a segment extract has not already been set if(empty($this->_extract_segment) === false) { throw new \LogicException('Extract segment options have already been set. You cannot call extractSegment more than once on a '.get_class($this).' object.'); } // check that a split has already been set as if it has we can't extract a segment // however we can extract a segment, then split it. if(empty($this->_split_options) === false) { throw new \LogicException('You cannot extract a segment once '.get_class($this).'::split has been called. You can however extract a segment, the call '.get_class($this).'::split.'); } // check the timecodes against the duration $duration = $this->readDuration(); if($from_timecode !== null && $duration->total_seconds < $from_timecode->total_seconds) { throw new \InvalidArgumentException('The duration of the media is less than the starting timecode specified.'); } else if($to_timecode !== null && $duration->total_seconds < $to_timecode->total_seconds) { throw new \InvalidArgumentException('The duration of the media is less than the end timecode specified.'); } $this->_extract_segment = array( 'preseek' => null, 'seek' => null, 'length' => null, ); // if the from timecode is greater than say 15 seconds, we will stream seek to 15 seconds before the // required extracted segment before the input to improve extract performance. // See http://ffmpeg.org/trac/ffmpeg/wiki/Seeking%20with%20FFmpeg $pre_input_stream_seek_offset = 0; $pre_input_stream_seek_adjustment = 15; if($from_timecode !== null) { if($accurate === false) { if($from_timecode->total_seconds > $pre_input_stream_seek_adjustment) { $pre_input_stream_seek_offset = $from_timecode->total_seconds-$pre_input_stream_seek_adjustment; $seek_timecode = new Timecode($pre_input_stream_seek_offset, Timecode::INPUT_FORMAT_SECONDS); $this->_extract_segment['preseek'] = $seek_timecode; } // if we have a pre input stream seek then input video is then offset by that ammount if($pre_input_stream_seek_offset > 0) { $from_timecode = new Timecode($pre_input_stream_seek_adjustment, Timecode::INPUT_FORMAT_SECONDS); } } // then seek the exact position after input $begin_position = $from_timecode->getTimecode('%hh:%mm:%ss.%ms', false); $this->_extract_segment['seek'] = $from_timecode; } else { $from_timecode = new Timecode(0, Timecode::INPUT_FORMAT_SECONDS); } // then add the number of seconds to export for if there is an end timecode. if($to_timecode !== null) { // if we have a pre input stream seek then input video is then offset by that ammount if($pre_input_stream_seek_offset > 0) { $to_timecode = new Timecode($to_timecode->total_seconds-$pre_input_stream_seek_offset, Timecode::INPUT_FORMAT_SECONDS); } $this->_extract_segment['length'] = $to_timecode->total_seconds - $from_timecode->total_seconds; } return $this; } /** * Splits (aka ffmpeg segment) the output into multiple files. * * @access public * @author Oliver Lillie * @return self */ public function split($split_by, $time_delta=0, $output_list_path=null) { // check that segment is available to ffmpeg $ffmpeg = new FfmpegParser($this->_config); $formats = $ffmpeg->getFormats(); if(isset($formats['segment']) === false) { throw new Exception('Unable to split media as the ffmpeg option "-segment" is not supported by your version of ffmpeg.'); } // check to see if split options are already set if(empty($this->_split_options) === false) { throw new \LogicException('Split options have already been set. You cannot call split more than once on a '.get_class($this).' object.'); } $this->_split_options = array(); $duration = $this->readDuration(); // check the split by if(empty($split_by) === true) { throw new \InvalidArgumentException('The split by value is empty, in \\PHPVideoToolkit\\'.get_class($this).'::split'); } // if we have an array, it's either timecodes (seconds) or integers (frames) else if(is_array($split_by) === true) { // we check to see if we have a timecode object, if we do then we are spliting at exact points if(is_object($split_by[0]) === true) { $times = array(); foreach ($split_by as $key=>$timecode) { if(get_class($timecode) !== 'PHPVideoToolkit\Timecode') { throw new \InvalidArgumentException('The split by timecode specified in index '.$key.' is not a \\PHPVideoToolkit\\Timecode object.'); } // check the timecode against the total number of seconds in the media duration. $seconds = $timecode->total_seconds; if($seconds > $duration->total_seconds) { throw new \InvalidArgumentException('The split by timecode specified in index '.$key.' is greater than the duration of the media ('.$duration->total_seconds.' seconds).'); } array_push($times, $seconds); } $this->_split_options['segment_times'] = implode(',', $times); } // otherwise we are spliting at frames else { $times = array(); foreach ($split_by as $key=>$integer) { if(is_int($integer) === false) { throw new \InvalidArgumentException('The split by frame number specified in index '.$key.' is not an integer.'); } // check the frame number against the total number of frames in the media duration. // TODO total frame rate comparison // $seconds = ceil($timecode->total_seconds); // if($seconds > $duration->total_seconds) // { // throw new Exception('The split by timecode specified in index '.$key.' is greater than the duration of the media ('.$duration->total_seconds.' seconds).'); // } // array_push($times, $integer); } $this->_split_options['segment_frames'] = implode(',', $times); } } // anything else is treated as an integer of which each split is the same length. else { if($split_by < 1) { throw new \InvalidArgumentException('The split by value must be >= 1, in \\PHPVideoToolkit\\'.get_class($this).'::split'); } // check the split time against the total number of seconds in the media duration. if($split_by > $duration->total_seconds) { throw new \InvalidArgumentException('The split by value is greater than the duration of the media ('.$duration->total_seconds.' seconds).'); } $this->_split_options['segment_time'] = (int) $split_by; } // check time delta if($time_delta < 0) { throw new \InvalidArgumentException('The time delta specified "'.$time_delta.'", in \\PHPVideoToolkit\\'.get_class($this).'::split must be >= 0'); } else if($time_delta > 0) { $this->_split_options['segment_time_delta'] = (float) $time_delta; } // check the directory that contains the output list is writeable if(empty($output_list_path) === false) { $output_list = realpath($output_list_path); $output_list_dir = dirname($output_list); if(is_dir($output_list_dir) === false) { throw new \InvalidArgumentException('The directory for the output list file "'.$output_list_path.'" does not exist, in \\PHPVideoToolkit\\'.get_class($this).'::split'); } else if(is_writeable($output_list_dir) === false) { throw new \InvalidArgumentException('The directory for the output list file "'.$output_list_path.'" is not writeable, in \\PHPVideoToolkit\\'.get_class($this).'::split'); } $this->_split_options['segment_list'] = $output_list_path; } // mark that we require a %d (or in phpvideotoolkits case %index or %timecode) in the file name output as multiple files will be outputed. $this->_require_d_in_output = true; return $this; } /** * Returns the FfmpegProcess object by reference. * * @access public * @author Oliver Lillie * @return FfmpegProcess */ public function &getProcess() { return $this->_process; } /** * Gets the final length of the output based upon the extraction/split commands * If the output is bing split, then an array will be returned, otherwise a float. * IMPORTANT! The duration(s) returned are based of various configurable options * and the resulting output by ffmpeg may vary slightly. * * @access public * @author Oliver Lillie * @return mixed */ public function getEstimatedFinalDuration() { $duration = $this->readDuration(); $duration_seconds = $duration->total_seconds; // as extractSegment must always be called before split therefore we process the segment options first if(empty($this->_extract_segment) === false) { if(empty($this->_extract_segment['length']) === false) { $duration_seconds = $this->_extract_segment['length']; } else { if(empty($this->_extract_segment['preseek']) === false) { $duration_seconds -= $this->_extract_segment['preseek']->total_seconds; } if(empty($this->_extract_segment['seek']) === false) { $duration_seconds -= $this->_extract_segment['seek']->total_seconds; } } $duration = new Timecode($duration_seconds, Timecode::INPUT_FORMAT_SECONDS); } // do we have any split options? if(empty($this->_split_options) === false) { // TODO // segment_time, a single time in seconds // segment_times, multiple times in seconds // segment_frames, frames } return $duration; } /** * Returns a string value of a portable identifier used in conjunction with ProgressHandlerPortable. * WARNING. If this function is called it automatically disables the garbage collection of the ExceBuffer. * * @access public * @author Oliver Lillie * @return string */ public function getPortableId() { return $this->_process->getPortableId().'.'.$this->getEstimatedFinalDuration()->total_seconds; } /** * Registers an output post process function, that is called after output has been generated. * It is important to note, that when an output post process is registered, the conversion * must then become blocking. * * @access protected * @author Oliver Lillie * @param Function $callback * @return self */ public function registerOutputPostProcess($callback, $args=array()) { if(is_callable($callback) === false) { throw new \InvalidArgumentException('The callback "'.$callback.'" is not callable.'); } if(is_array($args) === false) { throw new \InvalidArgumentException('The $args argument is not an array.'); } array_push($this->_post_process_callbacks, array($callback, $args)); // if a callback has been supplied then the process becomes blocking and must be set. $this->_blocking = true; return $this; } /** * The callback intentionally public, but should be regarded as protected that is used * to post process the output of a save command. * * @access protected * @author Oliver Lillie * @return mixed */ public function _postProcessOutput(FfmpegProcess $process) { $output = $process->completeProcess(); if(empty($this->_post_process_callbacks) === false) { foreach ($this->_post_process_callbacks as $callback) { $args = $callback[1]; array_unshift($args, $output, $this); $output = call_user_func_array($callback[0], $args); } } return $output; } /** * Converts a string path and output format into a MultiOutput object. * * @access protected * @author Oliver Lillie * @param string $save_path The string based path of a MultiObject. * @param Format $output_format An output format object. * @return MultiObject */ protected function _convertOutputPathToMultiOutput($save_path=null, Format $output_format=null) { $class = 'PHPVideoToolkit\MultiOutput'; // prevents unneccesary autoload. if($save_path instanceof $class === true) { return $save_path; } $multi_output = new MultiOutput($this->_config); $multi_output->setDefaultOutputFormat($this->getDefaultFormatClassName()); $multi_output->addOutput($save_path, $output_format); return $multi_output; } /** * Saves any changes to the media file to the given save path. * IMPORTANT! This save blocks PHP execution, meaning that once called, the PHP interpretter * will NOT continue untill the video/audio/media file(s) have been transcoded. * * @access public * @author Oliver Lillie * @param MultiOutput $save_path If a string then it is treated as a single output and the argument is the output path * of the generated file, otherwise if a PHPVideoToolkit\MultiOutput object is given then we treat the output * as multiple output. * @param Format $output_format It is the output format for the saved file. * @param string $overwrite One of the following constants determining the overwrite status of the save. * Media::OVERWRITE_FAIL - the save call will fail with an excetion if the save path already exists. * Media::OVERWRITE_EXISTING - the save call will overwrite any existing file with the same name. * Media::OVERWRITE_UNIQUE - the save call will augment the save path with a unique hash so that if a file with * the same name exists then there is no overwrite. * @param ProgressHandlerAbstract $progress_handler The progress handler object to supply to the save process. * @return mixed If the blocking mode of the process is set to block, the it returns a new * Media object on a successfull completion, otherwise an exception is thrown. If the blocking * mode is non blocking then the underlying FfmpegProcess is returned. */ public function save($save_path=null, Format $output_format=null, $overwrite=Media::OVERWRITE_FAIL, ProgressHandlerAbstract &$progress_handler=null) { $this->_saveAddInputFormatCommands($this->_media_input_format); // set the input files. $this->_process->setInputPath($this->_media_file_path); // loop and process the save path to multioutput so we can loop $multi_output = $this->_convertOutputPathToMultiOutput($save_path, $output_format); $index = 0; foreach ($multi_output as $save_path => $output_format) { // increment the output index so the process moves on to the next process to build if the loop continues. $this->_process->setOutputIndex($index); $index += 1; // pre process all of the common functionality and pre process the output format. $this->_savePreProcess($output_format, $save_path, $overwrite); // add the commands from the output format to the exec buffer // NOTE; this cannot be done in _savePreProcess as it must be done after, to ensure all the subclass // _savePreProcess functionality and main media class functionality is properly executed. $this->_saveAddOutputFormatCommands($output_format); // update the output path as the processing path? $this->_process->setOutputPath($this->_processing_path); } // set the progress handler if($progress_handler !== null) { $progress_handler->setTotalDuration($this->getEstimatedFinalDuration()); $this->_process->attachProgressHandler($progress_handler); } // exec the buffer // set the blocking mode // and execute the ffmpeg process. $buffer = $this->_process->setOutputPath($this->_processing_path) ->getExecBuffer() ->setBlocking($this->_blocking === null ? true : $this->_blocking); $process = $this->_process; $callback = array($this, '_postProcessOutput'); $buffer->registerCompletionCallback( function() use ($callback, $process) { call_user_func($callback, $process); } ); if($progress_handler !== null && $progress_handler->getNonBlockingCompatibilityStatus() === false) { $buffer->execute( function($exec_buffer, $null, $completed) use ($progress_handler, $callback, $process) { if($progress_handler !== null) { $progress_handler->callback(); } } ); } else { $buffer->execute(); } // just return the process if the process return $this->_process; } /** * Saves any changes to the media file to the given save path. * IMPORTANT! This save does NOT block PHP execution, meaning that once called, the PHP interpretter * will IMMEDIATELY continue. PHP will continue, in all likelyhood, exit before the ffmpeg has * completed the transcoding of any output. * * If you need to monitor the output for completion or processing then you can supplied a progress handler to * return information about the process. * * @access public * @author Oliver Lillie * @param string $save_path * @param Format $output_format * @param string $overwrite * @return boolean If the command is sent without error then true is returned, otherwise false. * The last error message is set to Media->last_error_message. A full list of error messages is * available through Media->error_messages. */ public function saveNonBlocking($save_path=null, Format $output_format=null, $overwrite=self::OVERWRITE_FAIL, ProgressHandlerAbstract &$progress_handler=null) { // check to see if the blocking mode has already been set to true. If it has we cannot save // non-blocking and must trigger error. if($this->_blocking === true) { throw new \LogicException('The blocking mode has been enabled by a function that you have enabled, or a Format that you have supplied. As a result you cannot use saveNonBlocking() and must use the blocking save method save() instead.'); } // set the non blocking of the exec process $this->_blocking = false; // set the progress handler if($progress_handler !== null) { // because only certain types of handlers are compatible with non blocking saves we need to check for compatibility. if($progress_handler->getNonBlockingCompatibilityStatus() === false) { throw new \LogicException('The progress handler given is not compatible with a non blocking save. This typically means that you have supplied a callback function in the constructor of the progress handler. Any progress handler with a supplied callback blocks PHP. Instead you should call $handler->probe() after the saveNonBlocking function call to get the progress of the encode.'); } } return $this->save($save_path, $output_format, $overwrite, $progress_handler); } /** * All three save functions, save, saveNonBlocking and getExecutionCommand have common things they * have to do before they are processed. This function contains those execution "warm-up" procedures. * * @access protected * @author Oliver Lillie * @param Format $output_format * @param string $save_path * @param string $overwrite * @return void */ protected function _savePreProcess(Format &$output_format=null, &$save_path, $overwrite) { // do some processing on the input format // $this->_processInputFormat(); // if the save path is null then we are overwriting the existing media file. if($save_path === null) { $overwrite = self::OVERWRITE_UNIQUE; $save_path = $this->_media_file_path; } // do some pre processing of the output format $this->_processOutputFormat($output_format, $save_path, $overwrite); // check the save path. $has_timecode_or_index = false; $has_timecode = false; $has_index = false; $basename = basename($save_path); $save_dir = dirname($save_path); $save_dir = realpath($save_dir); if($save_dir === false || is_dir($save_dir) === false) { throw new \InvalidArgumentException('The directory that the output is to be saved to, "'.$save_dir.'" does not exist.'); } else if(is_writeable($save_dir) === false || is_readable($save_dir) === false) { throw new \RuntimeException('The directory that the output is to be saved to, "'.$save_dir.'" is not read-writeable.'); } // check to see if we have a split output name. // although this is technically still allowed by ffmpeg, phpvideotoolkit has depreciated %d in favour of its own %index else if(preg_match('/\%([0-9]*)d/', $save_path) > 0) { throw new \InvalidArgumentException('The output file appears to be using FFmpeg\'s %d notation for multiple file output. The %d notation is depreciated in PHPVideoToolkit in favour of the %index or %timecode notations.'); } // if a %index or %timecode output is added then we can't check for exact file existence // we can however check for possible interfering matches. else if(($has_timecode_or_index = (preg_match('/\%(timecode|[0-9]*(index))/', $save_path, $matches) > 0))) { $has_timecode = $matches[1] === 'timecode'; $has_index = isset($matches[2]) === true && $matches[2] === 'index'; } // check to see if we have to have a timecode or index in the output and that we actually have one. else if($has_timecode_or_index === false && $this->_require_d_in_output === true) { throw new \InvalidArgumentException('It is required that either "%timecode" or "%index" to the save path as more that one file is expected be outputed. When using %index, it is possible to specify a number to be padded with a specific amount of 0s. For example adding %5index.jpg will output files like 00001.jpg, 00002.jpg etc.'); } // otherwise check that a file exists and the overrwite status of the request. else { if(is_file($save_dir.DIRECTORY_SEPARATOR.$basename) === true && (empty($overwrite) === true || $overwrite === self::OVERWRITE_FAIL)) { throw new \LogicException('The output file already exists and overwriting is disabled.'); } else if(is_file($save_dir.DIRECTORY_SEPARATOR.$basename) === true && $overwrite === self::OVERWRITE_EXISTING && is_writeable($save_dir.DIRECTORY_SEPARATOR.$basename) === false) { throw new \LogicException('The output file already exists, overwriting is enabled however the file is not writable.'); } } $save_path = $save_dir.DIRECTORY_SEPARATOR.$basename; // check for a recognised output format, and if one is not supplied // then check the a the format has been set in the output format, if not through an error and exit $format = false; $ext = pathinfo($save_path, PATHINFO_EXTENSION); if(empty($ext) === false) { // check we have a format we know about. $format = Extensions::toBestGuessFormat($ext); } // if we still don't have a format, check from the output format. if(!$format) { $options = $output_format->getFormatOptions(); if(isset($options['format']) === false || empty($options['format']) === true) { if(empty($ext) === true) { throw new \LogicException('The output path of the file extension has not be given. Please either set a file extension of the output path - or - call setFormat() on the output format to set the format of the output media.'); } throw new \LogicException('Un-recognised file extension. Please call setFormat() on the output format to set the format of the output media.'); } } // process the overwrite status switch($overwrite) { case self::OVERWRITE_EXISTING : $this->_process->addCommand('-y'); break; // insert a unique id into the save path case self::OVERWRITE_UNIQUE : $pathinfo = pathinfo($save_path); $save_path = $pathinfo['dirname'].DIRECTORY_SEPARATOR.$pathinfo['filename'].'-u_'.Str::generateRandomString().'.'.$pathinfo['extension']; break; // this is purely in case the media object is "re-used", as if the command is already been set to overwrite // but the subsequent save is not, we must remove any previous command so we don't get unwanted overwrites. default : $this->_process->removeCommand('-y'); } $this->_output_path = $this->_processing_path = $save_path; // check to see if we are extracting a segment // It is important that we are the extract commands before any segmenting, so that if we are extracting // a segment then spliting the file everything goes as expected. if(empty($this->_extract_segment) === false) { if(empty($this->_extract_segment['preseek']) === false) { $this->_process->addPreInputCommand('-ss', $this->_extract_segment['preseek']->getTimecode('%hh:%mm:%ss.%ms', false)); } if(empty($this->_extract_segment['seek']) === false) { $this->_process->addCommand('-ss', $this->_extract_segment['seek']->getTimecode('%hh:%mm:%ss.%ms', false)); } if(empty($this->_extract_segment['length']) === false) { $this->_process->addCommand('-t', $this->_extract_segment['length']); } } // if we are splitting the output if(empty($this->_split_options) === false) { // if so check that a timecode or index has been set if($has_timecode_or_index === false) { $pathinfo = pathinfo($save_path); $save_path = $this->_output_path = $pathinfo['dirname'].DIRECTORY_SEPARATOR.$pathinfo['filename'].'-%timecode.'.$pathinfo['extension']; $has_timecode_or_index = true; } // if we are splitting we need to add certain commands to make it work. // one of those is -map 0. Also note that video and audio objects additionally set their own // codecs if not supplied, in their related class function _savePreProcess // TODO this may need to be changed dependant on the number of mappings. $this->_process->addCommand('-map', '0'); // -acodec copy // -vcodec copy // we must do this via add command rather than setFormat as it rejects the segment format. $this->_process->addCommand('-f', 'segment'); foreach ($this->_split_options as $command => $arg) { $this->_process->addCommand('-'.$command, $arg); } // get the output commands and augment with the final output options. $options = $output_format->getFormatOptions(); // set the split format if an output format has already been set. and remove from the output format so that multiple "-f" formats are not given to the buffer if(empty($options['format']) === false) { $this->_ignore_format = true; $this->_process->addCommand('-segment_format', $options['format']); } // TODO add time delta and segment_list } // check to see if we have any global meta if(empty($this->_metadata) === false) { $meta_string = array(); foreach ($this->_metadata as $key => $value) { $this->_process->addCommand('-metadata:g', $key.'='.$value.'', true); } } // if we have a timecode or index based path we then have to supply a temporary processing path so that // we can perform the rename to timecode and index after they items have been transcoded by ffmpeg. if($has_timecode_or_index === true) { $processing_path = $this->_output_path; if($has_timecode === true) { // we build the timecode and frame rate data into the output if we use %timecode // that way we can always reconstruct the timecode even from another script or process. // get the frame rate of the export. we give priority to "-r" as this is the output of the object if already set somewhere, // otherwise we revert to the output format setting, // then fallback to the to the framerate of the current video component if(($frame_rate = $this->_process->getCommand('-r')) === false) { $options = $output_format->getFormatOptions(); if(empty($options['video_frame_rate']) === false) { $frame_rate = $options['video_frame_rate']; } else { $data = $this->readVideoComponent(); if(isset($data['frames']) === true && isset($data['frames']['rate']) === true) { $frame_rate = $data['frames']['rate']; } } } if($frame_rate <= 0) { throw new \RuntimeException('Unable to access the output frame rate value and as a result we cannot generate a timecode based filename output.'); } else if(preg_match('/[0-9]+\/[0-9]+/', $frame_rate) > 0) { $frame_rate = explode('/', $frame_rate); $frame_rate = $frame_rate[0]/$frame_rate[1]; } // get the starting offset of the export $offset = '0'; $stream_seek_input = $this->_process->getPreInputCommand('-ss'); if($stream_seek_input !== false) { $offset += Timecode::parseTimecode($stream_seek_input, '%hh:%mm:%ss.%ms'); } $stream_seek_output = $this->_process->getCommand('-ss'); if($stream_seek_output !== false) { $offset += Timecode::parseTimecode($stream_seek_output, '%hh:%mm:%ss.%ms'); } // apply rounding to get a float of precise length $offset = round($offset, 2); $processing_path = preg_replace('/%timecode/', '.%12d.'.$frame_rate.'_'.$offset.'._t.', $processing_path); } if($has_index === true) { $processing_path = preg_replace('/%([0-9]*)index/', '.%$1d._i.', $processing_path); } // add a unique identifier to the processing path to prevent overwrites. $pathinfo = pathinfo($processing_path); $this->_processing_path = $pathinfo['dirname'].DIRECTORY_SEPARATOR.$pathinfo['filename'].'._u.'.Str::generateRandomString().'.u_.'.$pathinfo['extension']; } } /** * Process the output format just before the it is compiled into commands. * * @access protected * @author Oliver Lillie * @param Format &$output_format * @return void */ protected function _processOutputFormat(Format &$output_format=null, &$save_path, $overwrite) { // check to see if we have been set and output format, if not generate an empty one. if($output_format === null) { $format = null; $ext = pathinfo($save_path, PATHINFO_EXTENSION); if(empty($ext) === false) { $format = Extensions::toBestGuessFormat($ext); } $output_format = $this->getDefaultFormat(Format::OUTPUT, $format); } // set the media into the format object so that we can update the format options that // require a media object to process. $output_format->setMedia($this) ->updateFormatOptions($save_path, $overwrite); } /** * Adds the output format commands from the Format object to the FfmpegProcess * * @access protected * @author Oliver Lillie * @param Format $output_format * @return void */ protected function _saveAddOutputFormatCommands(Format $output_format=null) { if($output_format !== null) { $commands = $output_format->getCommandsHash(); if(empty($commands) === false) { foreach ($commands as $key => $value) { // this is a special consideration as if the format is being segmented then any output // format must be ignored as it is already being set to -segment_format. if($this->_ignore_format === true && $key === '-f') { continue; } $this->_process->addCommand($key, $value); } } } } /** * Adds the input format commands from the Format object to the FfmpegProcess * * @access protected * @author Oliver Lillie * @param Format $output_format * @return void */ protected function _saveAddInputFormatCommands(Format $input_format=null) { if($input_format !== null) { $commands = $input_format->getCommandsHash(); if(empty($commands) === false) { foreach ($commands as $key => $value) { $this->_process->addPreInputCommand($key, $value); } } } } // The below functions override the MediaParser functions so to automatically provide // the media_file_path each time. /** * Returns the information about a specific media file. * * @access public * @author Oliver Lillie * @param boolean $read_from_cache * @return array */ public function read($read_from_cache=true) { return parent::getFileInformation($this->_media_file_path, $read_from_cache); } /** * Returns the files duration as a Timecode object if available otherwise returns false. * * @access public * @author Oliver Lillie * @param boolean $read_from_cache * @return mixed Returns a Timecode object if the duration is found, otherwise returns null. */ public function readDuration($read_from_cache=true) { return parent::getFileDuration($this->_media_file_path, $read_from_cache); } /** * Returns the files duration as a Timecode object if available otherwise returns false. * * @access public * @author Oliver Lillie * @param boolean $read_from_cache * @return mixed Returns a Timecode object if the duration is found, otherwise returns null. */ public function readGlobalMetaData($read_from_cache=true) { return parent::getFileGlobalMetadata($this->_media_file_path, $read_from_cache); } /** * Returns the files bitrate if available otherwise returns -1. * * @access public * @author Oliver Lillie * @param boolean $read_from_cache * @return mixed Returns the bitrate as an integer if available otherwise returns -1. */ public function readBitrate($read_from_cache=true) { return parent::getFileBitrate($this->_media_file_path, $read_from_cache); } /** * Returns the start point of the file as a Timecode object if available, otherwise returns null. * * @access public * @author Oliver Lillie * @param boolean $read_from_cache * @return mixed Returns a Timecode object if the start point is found, otherwise returns null. */ public function readStart($read_from_cache=true) { return parent::getFileStart($this->_media_file_path, $read_from_cache); } /** * Returns the start point of the file as a Timecode object if available, otherwise returns null. * * @access public * @author Oliver Lillie * @param boolean $read_from_cache * @return mixed Returns a string 'audio' or 'video' if media is audio or video, otherwise returns null. */ public function readType($read_from_cache=true) { return parent::getFileType($this->_media_file_path, $read_from_cache); } /** * Returns any video information about the file if available. * * @access public * @author Oliver Lillie * @param boolean $read_from_cache * @return mixed Returns an array of found data, otherwise returns null. */ public function readVideoComponent($read_from_cache=true) { return parent::getFileVideoComponent($this->_media_file_path, $read_from_cache); } /** * Returns mean and max volume information of the file. * * @access public * @author Samar Rizvi * @param boolean $read_from_cache * @return mixed Returns an array of found data, otherwise returns null. */ public function readVolumeComponent($read_from_cache=true) { return parent::getFileVolumeComponent($this->_media_file_path, $read_from_cache); } /** * Returns any audio information about the file if available. * * @access public * @author Oliver Lillie * @param boolean $read_from_cache * @return mixed Returns an array of found data, otherwise returns null. */ public function readAudioComponent($read_from_cache=true) { return parent::getFileAudioComponent($this->_media_file_path, $read_from_cache); } /** * Returns a boolean value determined by the media having an audio channel. * * @access public * @author Oliver Lillie * @param boolean $read_from_cache * @return boolean */ public function readHasAudio($read_from_cache=true) { return parent::getFileHasAudio($this->_media_file_path, $read_from_cache); } /** * Returns a boolean value determined by the media having a video channel. * * @access public * @author Oliver Lillie * @param boolean $read_from_cache * @return boolean */ public function readHasVideo($read_from_cache=true) { return parent::getFileHasVideo($this->_media_file_path, $read_from_cache); } /** * Returns the raw data provided by ffmpeg about a file. * * @access public * @author Oliver Lillie * @param boolean $read_from_cache * @return mixed Returns false if no data is returned, otherwise returns the raw data as a string. */ public function readRawInformation($read_from_cache=true) { return parent::getFileRawInformation($this->_media_file_path, $read_from_cache); } }