/** * 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.'); } 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.'); } 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'; } 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.'); } 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 // 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_' . String::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. // 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.' . String::generateRandomString() . '.u_.' . $pathinfo['extension']; } }
/** * Renames any output from ffmpeg that would have been outputted in a sequence, ie using %d. Typically used with imagery. * * @access public * @author Oliver Lillie * @param string $output_path The string notation for the output path. * @return array Returns an array of modified file paths. */ protected function _renamePercentDOutput($output_path) { $output = array(); // we have the output path but we now need to treat differently dependant on if we have multiple file output. if (preg_match('/\\.(\\%([0-9]*)d)\\.([0-9\\.]+_[0-9\\.]+\\.)?_(i|t)\\./', $output_path, $matches) > 0) { // determine what we have to rename all the files to. $convert_back_to = $matches[4] === 't' ? 'timecode' : (int) $matches[2]; // get the glob path and then find all the files from this output $output_glob_path = str_replace($matches[0], '.*.' . $matches[3] . '_' . $matches[4] . '.', $output_path); $outputted_files = glob($output_glob_path); // sort the output naturally so that if there is no index padding that we get the frames in the correct order. natsort($outputted_files); // loop to rename the file and then create each output object. $timecode = null; foreach ($outputted_files as $path) { if ($convert_back_to === 'timecode') { // if the start timecode has not been generated then find the required from the path string. if ($timecode === null) { $matches[3] = rtrim($matches[3], '.'); $matches[3] = explode('_', $matches[3]); $timecode = new Timecode($matches[3][1], Timecode::INPUT_FORMAT_SECONDS, $matches[3][0]); } else { $timecode->frame += 1; } $actual_path = preg_replace('/\\.[0-9]{12}\\.[0-9\\.]+_[0-9\\.]+\\._t\\./', $timecode->getTimecode('%hh_%mm_%ss_%ms', false), $path); } else { $actual_path = preg_replace('/\\.([0-9]+)\\._i\\./', '$1', $path); } $actual_path = preg_replace('/\\._u\\.[0-9]{5}_[a-z0-9]{5}_[0-9]+\\.u_\\./', '.', $actual_path); rename($path, $actual_path); array_push($output, $actual_path); } unset($outputted_files); // TODO create the multiple image output } return $output; }
/** * Once the process has been completed this function can be called to return the output * of the process. Depending on what the process is outputting depends on what is returned. * If a single video or audio is being outputted then the related PHPVideoToolkit media object * will be returned. However if multiple files are being outputed then an array of the associated * objects are returned. Typically speaking an array will be returned when %index or %timecode * are within the output path. * * @access public * @author Oliver Lillie * @return mixed */ public function getOutput($post_process_callback = null) { if ($this->isCompleted() === false) { throw new FfmpegProcessOutputException('Encoding has not yet started.'); } // check for an error. if ($this->hasError() === true) { // check for specific recieved signal errors. $last_split = $this->getLastSplit(); if (preg_match('/Received signal ([0-9]+): terminating\\./', $last_split, $matches) > 0) { $kill_signals = array(1 => 'Hang up detected on controlling terminal or death of controlling process.', 2 => 'User sent an interrupt signal.', 3 => 'User sent a quit signal.', 4 => 'Illegal instruction.', 6 => 'Abort signal from abort(3).', 8 => 'Floating point exception.', 9 => 'Kill signal sent.', 11 => 'Invalid memory reference', 13 => 'Broken pipe: write to pipe with no readers', 14 => 'Timer signal from alarm(2)', 15 => 'Termination signal sent.', 24 => 'Imposed time limit ({length} seconds) exceeded.'); // TODO add more signals. $kill_int = (int) $matches[1]; if (isset($kill_signals[$kill_int]) === true) { $message = $kill_signals[$kill_int]; if ($kill_int == 24) { $length = $this->getCommand('-timelimit'); $length = !$length ? 'unknown' : $length; $message = str_replace('{length}', $length, $message); } throw new FfmpegProcessOutputException('Process was aborted. ' . $message); } else { throw new FfmpegProcessOutputException('Termination signal received and the process aborted. Signal was ' . $matches[1]); } } throw new FfmpegProcessOutputException('Encoding failed and an error was returned from ffmpeg. Error code ' . $this->getErrorCode() . ' was returned the message (if any) was: ' . $last_split); } if ($post_process_callback !== null) { if (is_callable($post_process_callback) === false) { throw new Exception('The supplied post proces scallback is not callable.'); } } // get the output of the process $output_path = $this->getOutputPath(); // we have the output path but we now need to treat differently dependant on if we have multiple file output. if (preg_match('/\\.(\\%([0-9]*)d)\\.([0-9\\.]+_[0-9\\.]+\\.)?_(i|t)\\./', $output_path, $matches) > 0) { // determine what we have to rename all the files to. $convert_back_to = $matches[4] === 't' ? 'timecode' : (int) $matches[2]; // get the glob path and then find all the files from this output $output_glob_path = str_replace($matches[0], '.*.' . $matches[3] . '_' . $matches[4] . '.', $output_path); $outputted_files = glob($output_glob_path); // sort the output naturally so that if there is no index padding that we get the frames in the correct order. natsort($outputted_files); // loop to rename the file and then create each output object. $output = array(); $timecode = null; foreach ($outputted_files as $path) { $actual_path = preg_replace('/\\._u\\.[0-9]{5}_[a-z0-9]{5}_[0-9]+\\.u_\\./', '.', $path); if ($convert_back_to === 'timecode') { // if the start timecode has not been generated then find the required from the path string. if ($timecode === null) { $matches[3] = rtrim($matches[3], '.'); $matches[3] = explode('_', $matches[3]); $timecode = new Timecode($matches[3][1], Timecode::INPUT_FORMAT_SECONDS, $matches[3][0]); } else { $timecode->frame += 1; } $actual_path = preg_replace('/\\.[0-9]{12}\\.[0-9\\.]+_[0-9\\.]+\\._t\\./', $timecode->getTimecode('%hh_%mm_%ss_%ms', false), $actual_path); } else { $actual_path = preg_replace('/\\.([0-9]+)\\._i\\./', '$1', $actual_path); } rename($path, $actual_path); $media_class = $this->_findMediaClass($actual_path); $output_object = new $media_class($actual_path, $this->_config, null, false); array_push($output, $output_object); unset($output_object); } unset($outputted_files); // TODO create the multiple image output } else { // check for a none multiple file existence if (empty($output_path) === true) { throw new FfmpegProcessOutputException('Unable to find output for the process as it was not set.'); } else { if (is_file($output_path) === false) { throw new FfmpegProcessOutputException('The output "' . $output_path . '", of the Ffmpeg process does not exist.'); } else { if (filesize($output_path) <= 0) { throw new FfmpegProcessOutputException('The output "' . $output_path . '", of the Ffmpeg process is a 0 byte file. Something must have gone wrong however it wasn\'t reported as an error by FFmpeg.'); } } } // get the media class from the output. // create the object from the class name and return the new object. $media_class = $this->_findMediaClass($output_path); $output = new $media_class($output, $this->_config, null, false); } // do any post processing callbacks if ($post_process_callback !== null) { $output = call_user_func($post_process_callback, $output, $this); } // finally return the output to the user. return $output; }
/** * @param Timecode $timecode * * @return Timecode */ public function subtract(Timecode $timecode) : Timecode { $this->fromSeconds($this->getSeconds() - $timecode->getSeconds()); return $this; }
echo 'new Timecode(102.34); = ' . $timecode . '<br />'; $timecode = new Timecode(102.34, Timecode::INPUT_FORMAT_SECONDS); echo 'new Timecode(102.34, Timecode::INPUT_FORMAT_SECONDS); = ' . $timecode . '<br />'; $timecode = new Timecode(1.705666667, Timecode::INPUT_FORMAT_MINUTES); echo 'new Timecode(1.705666667, Timecode::INPUT_FORMAT_MINUTES); = ' . $timecode . '<br />'; $timecode = new Timecode(0.028427778, Timecode::INPUT_FORMAT_HOURS); echo 'new Timecode(.028427778, Timecode::INPUT_FORMAT_HOURS); = ' . $timecode . '<br />'; $timecode = new Timecode('00:01:42.34', Timecode::INPUT_FORMAT_TIMECODE); echo 'new Timecode(\'00:01:42.34\', Timecode::INPUT_FORMAT_TIMECODE); = ' . $timecode . '<br />'; $timecode = new Timecode(60); echo 'new Timecode(60); = ' . $timecode . '<br />'; $timecode = new Timecode(360); echo 'new Timecode(360); = ' . $timecode . '<br />'; echo '<hr />'; echo '<h2>Adjusting timecode values</h2>'; $timecode = new Timecode('00:01:42.34', Timecode::INPUT_FORMAT_TIMECODE, 24); echo '$timecode = new Timecode(\'00:01:42.34\', Timecode::INPUT_FORMAT_TIMECODE); = ' . $timecode . '<br />'; $adjustments = array(array(15, 'hours', true), array(-54102.34, 'seconds', true), array(-99, 'milliseconds', true), array(59, 'seconds', true), array(1, 'seconds', false), array(59, 'seconds', true), array(999, 'milliseconds', true), array(1, 'milliseconds', true), array(48, 'frames', false), array(-15, 'frames', true), array(-1, 'seconds', true), array(-375, 'milliseconds', true)); foreach ($adjustments as $value) { if ($value[2] === true) { $timecode->{$value[1]} += $value[0]; echo '$timecode->' . $value[1] . ' += ' . $value[0] . '; // = ' . $timecode->getTimecode('%hh:%mm:%ss:%ms') . '<br />'; } else { echo '<Br />$timecode->reset();<br />'; $timecode->reset(); $timecode->{$value[1]} = $value[0]; echo '$timecode->' . $value[1] . ' = ' . $value[0] . '; // = ' . $timecode->getTimecode('%hh:%mm:%ss:%ms') . '<br />'; } } echo '<hr />'; echo '<h2>Setting a timecode value</h2>';