* @license PHP License * @package WB * @subpackage vfs */ WBClass::load('WBVFS_Mime' , 'WBString' ); /** * Virtual File System: Mime Video * * * * @version 0.2.0 * @package WB * @subpackage vfs */ class WBVFS_Mime_Video extends WBVFS_Mime { /** * convert command * @link http://www.imagemagick.org */ protected static $magickConvert = 'convert'; /** * ffmpeg command * @link http://www.ffmpeg.org/ */ protected static $ffmpeg = 'ffmpeg'; /** * file name extension * @var string */ protected $extension = 'flv'; /** * major mime type * @var string */ protected $mimeMajor = 'video'; /** * configuration * @var WBConfig */ private $config; /** * initialize tools * * set command line tools */ public static function staticConstruct() { self::$ffmpeg = WBParam::get('wb/vfs/mime/ffmpeg', self::$ffmpeg); self::$magickConvert = WBParam::get('wb/vfs/mime/imagemagick/convert', self::$magickConvert); } /** * 2nd constructor * */ protected function init() { $this->config = WBClass::create('WBConfig'); $this->config->load('vfs/transcode/video'); } /** * get information about this file * * @return array */ public function getInfo() { $ret = null; $out = array(); $cmd = sprintf('%s -vstats -i %s 2>&1', self::$ffmpeg, $this->file); exec($cmd, $out, $ret); $this->info = $this->parseFfprobe($out); return $this->info; } /** * Parse output of FFprobe * * @param array * @return array */ public function parseFfprobe($out) { $mediaInfo = array(); $mediaInfo['container'] = null; $mediaInfo['duration'] = null; $mediaInfo['seconds'] = 0; $mediaInfo['bitrate'] = null; $mediaInfo['width'] = ''; $mediaInfo['height'] = ''; $mediaInfo['streams'] = array(); // extract info from video stats $streamVideo = array(); $inInput = false; foreach( $out as $o ) { // get rid of whitespaces $o = trim($o); if (!$inInput) { if (strncmp('Input #', $o, 7) != 0) { continue; } $inInput = true; // find container format $tmp = array_map('trim', explode(',', $o)); $mediaInfo['container']= $tmp[1]; continue; } // left output? if (strncmp('Output #', $o, 8) == 0) { $inInput = false; continue; } // duration and bitrate if (strncmp('Duration: ', $o, 10) == 0) { if (preg_match_all('/(\w+):\s([^,]+)/', $o, $match)) { if (strtolower( $match[1][0] ) == 'duration') { $mediaInfo['duration'] = $match[2][0]; } if (isset($match[1][2]) && strtolower($match[1][2]) == 'bitrate') { list($mediaInfo['bitrate']) = explode(' ', $match[2][2]); } } $time = array_reverse(explode(':', $mediaInfo['duration'])); $mul = 1; foreach ($time as $t) { $mediaInfo['seconds'] += $t * $mul; $mul *= 60; } // go to next line continue; } // find streams if (strncmp('Stream #', $o, 8) == 0 ) { $tmp = array_map('trim', explode(' ', $o)); $stream = array( 'name' => substr($tmp[1], 1, -1), 'type' => strtolower($tmp[2]) ); array_shift($tmp); array_shift($tmp); $tmp = implode(' ', $tmp); $tmp = array_map('trim', explode(':', $tmp)); $stream['type'] = strtolower($tmp[0]); $typeInfo = array_map('trim', explode(',', $tmp[1])); switch ($stream['type']) { case 'video': $dim = explode('x', $typeInfo[2]); list($dim[1]) = explode(' ', $dim[1]); $stream['codec'] = $typeInfo[0]; $stream['size'] = implode('x', $dim); $stream['par'] = '1:1'; $stream['dar'] = '1:1'; if (isset($typeInfo[3])) { list($stream['fps']) = explode(' ', $typeInfo[3]); } // PAR if( preg_match( '/Video:.+PAR\s(\d+:\d+)/', $o, $match ) ) { $stream['par'] = $match[1]; } // DAR if( preg_match( '/Video:.+DAR\s(\d+:\d+)/', $o, $match ) ) { $stream['dar'] = $match[1]; } $streamVideo = $stream; $streamVideo['width'] = $dim[0]; $streamVideo['height'] = $dim[1]; break; case 'audio': $stream['codec'] = $typeInfo[0]; list($stream['samplingrate']) = explode(' ', $typeInfo[1]); $stream['channel'] = $typeInfo[2]; if (isset($typeInfo[3])) { list($stream['bitrate']) = explode(' ', $typeInfo[3]); } break; } $mediaInfo['streams'][] = $stream; } } // calc width and height according aspect ration if (empty($streamVideo)) { return $mediaInfo; } // calculate real size for anamorphic videos $size = array( $streamVideo['width'], $streamVideo['height'] ); $this->calcRealSize($size, $streamVideo['par']); $mediaInfo['width'] = $size[0]; $mediaInfo['height'] = $size[1]; return $mediaInfo; } /** * import file * */ public function import() { } /** * queue file! * * @return bool */ public function queue() { return true; } /** * Execute command * * Commands: * - setposter: change poster image * * @param string $cmd * @param array $arg */ public function execute($cmd, $arg = array()) { $cmd = trim(strtolower($cmd)); $log = array( 'file' => $this->vfile->getId(), 'mime' => $this->mimeMajor, 'cmd' => $cmd, 'status' => 'done' ); switch ($cmd) { case 'setposter': $data = base64_decode(substr($arg['data'], 23)); if (false === $data) { return; } $cachDir = $this->vfile->getCacheDir(true); $this->vfile->flushCache('image'); file_put_contents($cachDir . '/image/image', $data); chmod($cachDir . '/image/image', 0666); break; default: $log['status'] = 'commandunknown'; $this->log->err($log); return; break; } $this->log->notice($log); } /** * get file name of requested file * * @return string */ protected function doGetRequestedFile(&$redirect) { $this->file = $this->vfile->getPath(); $org = array($this->vfile->getInfo('width'), $this->vfile->getInfo('height')); $prefix = strtolower($this->requestedMimeMinor); if (0 == strncmp($prefix, 'x-', 2)) { $prefix = substr($prefix, 2); } $this->extension = $this->config->get($prefix . '/extension', $prefix); $this->mimeMinor = $this->config->get($prefix . '/mimeminor', 'x-' . $prefix); $cacheDir = $this->mkCacheDir(); if (!file_exists($cacheDir . '/' . $prefix)) { // this sort of locks the file an prevents multi processings touch($cacheDir . '/' . $prefix); chmod($cacheDir . '/' . $prefix, 0666); $this->transcode($org, $prefix, $cacheDir); } $this->file = $cacheDir . '/' . $prefix; return $this->file; } /** * Execute transcoding * * Set current file to file the one coresponding to requested mime minor type * Find matching transcoding targets in config. Populate and execute transcoding * commands. If there is a faststart tag, execute post processing command * * @param array $org original video dimensions * @param string $prefix * @param string $cacheDir */ private function transcode($org, $prefix, $cacheDir) { $now = WBClock::now(); $cwd = $this->mkWorkingDir(); $des = 'out.' . $prefix; $size = array( intval($this->config->get($prefix . '/boundingbox/width', '640')), intval($this->config->get($prefix . '/boundingbox/height', '360')) ); $this->calcSize($size, $org); // multi or single pass commands $cmd = $this->config->get($prefix . '/template/cmd', '{FFMPEG} -i {INFILE} -s {SIZE} -f mp4 -y {OUTFILE} 2&>1'); if (!is_array($cmd)) { $cmd = array($cmd); } $vars = array( 'cwd' => $cwd, 'presetdir' => WBParam::get('wb/dir/system') . '/resource/mime/video/ffmpeg', 'ffmpeg' => self::$ffmpeg, 'infile' => $this->file, 'outfile' => $des, 'size' => implode('x', $size), 'option_audio' => implode(' ', $this->config->get($prefix . '/option/audio', array())), 'option_video' => implode(' ', $this->config->get($prefix . '/option/video', array())), ); $oldcwd = getcwd(); chdir($cwd); // execute all commands foreach ($cmd as $c) { $c = WBString::populate($c, $vars); $out = array(); $ret = null; exec($c, $out, $ret); if ($ret > 0) { $log = array( 'file' => basename($this->file), 'size' => filesize($this->file), 'mime' => 'video', 'action' => 'convert', 'prefix' => $prefix, 'cmd' => $c ); $this->log->err($log); } } // qt-faststart $cmd = $this->config->get($prefix . '/template/faststart'); if (!empty($cmd)) { $vars = array( 'infile' => $des, 'outfile' => $cacheDir . '/' . $prefix, ); $cmd = WBString::populate($cmd, $vars); $out = array(); $ret = null; exec($cmd, $out, $ret); } else { rename($des, $cacheDir . '/' . $prefix); } chdir($oldcwd); $this->rmWorkingDir($cwd); chmod($cacheDir . '/' . $prefix, 0666); $log = array( 'file' => basename($this->file), 'size' => filesize($this->file), 'mime' => 'video', 'action' => 'convert', 'target' => $prefix, 'elapsed' => WBClock::stop($now, 1000, 1) . 'ms' ); $this->log->notice($log); } /** * Create temporary working dir * * Receive path of dir * * @return string */ private function mkWorkingDir() { $tmpDir = WBParam::get('wb/dir/base') . '/var/tmp'; $cwd = tempnam($tmpDir, 'vfsVideo'); unlink($cwd); $cwd = $cwd . '.d'; mkdir($cwd); return $cwd; } /** * Remove working dir * * Delete folder recursively * * @see WBFile::removeDir() * @param string $cwd */ private function rmWorkingDir($cwd) { $file = WBClass::create('WBFile'); $file->removeDir($cwd, false); } /** * Caluclate new video size * * Avoid up-scaling. Keep aspect. Fit to bounding box * * @param array $size requested image size * @param array $org */ private function calcSize(&$size, $org) { $maxWidth = $size[0]; // get resize factor $divX = $size[0] / $org[0]; $divY = $size[1] / $org[1]; $factor = min($divX, $divY); // make sure there is at least one pixel in width and height $size[0] = max(1, round($org[0] * $factor)); $size[1] = max(1, round($org[1] * $factor)); // width and height must be a multiple of 2 $size[1] = round(($size[1] / 2), 0) * 2; $size[0] = round(($size[0] / 2), 0) * 2; } /** * Calculate view size of video * * Check pixel asect ratio (PAR) and calculate actual video * size. This allows to transform from anamorphic to square pixel * videos. (Square pixel is what we prefer) * * @param array $size * @param string $par */ private function calcRealSize(&$size, $par) { if ('1:1' == $par) { return; } $par = explode(':', $par); $par = intval($par[0]) / intval($par[1]); $size[0] *= $par; $size[0] = round(($size[0] / 2), 0) * 2; } }