Pony.fm/vendor/codescale/ffmpeg-php/FFmpegMovie.php

766 lines
21 KiB
PHP
Raw Normal View History

2013-07-25 23:33:04 +02:00
<?php
/**
* FFmpegMovie represents a movie file
*
* @author char0n (Vladimír Gorej, gorej@codescale.net)
* @package FFmpegPHP
* @license New BSD
* @version 2.6
*/
class FFmpegMovie implements Serializable {
protected static $REGEX_DURATION = '/Duration: ([0-9]{2}):([0-9]{2}):([0-9]{2})(\.([0-9]+))?/';
protected static $REGEX_FRAME_RATE = '/([0-9\.]+\sfps,\s)?([0-9\.]+)\stbr/';
protected static $REGEX_COMMENT = '/comment\s*(:|=)\s*(.+)/i';
protected static $REGEX_TITLE = '/title\s*(:|=)\s*(.+)/i';
protected static $REGEX_ARTIST = '/(artist|author)\s*(:|=)\s*(.+)/i';
protected static $REGEX_COPYRIGHT = '/copyright\s*(:|=)\s*(.+)/i';
protected static $REGEX_GENRE = '/genre\s*(:|=)\s*(.+)/i';
protected static $REGEX_TRACK_NUMBER = '/track\s*(:|=)\s*(.+)/i';
protected static $REGEX_YEAR = '/year\s*(:|=)\s*(.+)/i';
protected static $REGEX_FRAME_WH = '/Video:.+?([1-9][0-9]*)x([1-9][0-9]*)/';
protected static $REGEX_PIXEL_FORMAT = '/Video: [^,]+, ([^,]+)/';
protected static $REGEX_BITRATE = '/bitrate: ([0-9]+) kb\/s/';
protected static $REGEX_VIDEO_BITRATE = '/Video:.+?([0-9]+) kb\/s/';
protected static $REGEX_AUDIO_BITRATE = '/Audio:.+?([0-9]+) kb\/s/';
protected static $REGEX_AUDIO_SAMPLE_RATE = '/Audio:.+?([0-9]+) Hz/';
protected static $REGEX_VIDEO_CODEC = '/Video:\s([^,]+),/';
protected static $REGEX_AUDIO_CODEC = '/Audio:\s([^,]+),/';
protected static $REGEX_AUDIO_CHANNELS = '/Audio:\s[^,]+,[^,]+,([^,]+)/';
protected static $REGEX_HAS_AUDIO = '/Stream.+Audio/';
protected static $REGEX_HAS_VIDEO = '/Stream.+Video/';
protected static $REGEX_ERRORS = '/.*(Error|Permission denied|could not seek to position|Invalid pixel format|Unknown encoder|could not find codec|does not contain any stream).*/i';
/**
* FFmpeg binary
*
* @var string
*/
protected $ffmpegBinary;
/**
* Output provider
*
* @var OutputProvider
*/
protected $provider;
/**
* Movie file path
*
* @var string
*/
protected $movieFile;
/**
* provider output
*
* @var string
*/
protected $output;
/**
* Movie duration in seconds
*
* @var float
*/
protected $duration;
/**
* Current frame index
*
* @var int
*/
protected $frameCount;
/**
* Movie frame rate
*
* @var float
*/
protected $frameRate;
/**
* Comment ID3 field
*
* @var string
*/
protected $comment;
/**
* Title ID3 field
*
* @var string
*/
protected $title;
/**
* Author ID3 field
*
* @var string
*/
protected $artist;
/**
* Copyright ID3 field
*
* @var string
*/
protected $copyright;
/**
* Genre ID3 field
*
* @var string
*/
protected $genre;
/**
* Track ID3 field
*
* @var int
*/
protected $trackNumber;
/**
* Year ID3 field
*
* @var int
*/
protected $year;
/**
* Movie frame height
*
* @var int
*/
protected $frameHeight;
/**
* Movie frame width
*
* @var int
*/
protected $frameWidth;
/**
* Movie pixel format
*
* @var string
*/
protected $pixelFormat;
/**
* Movie bit rate combined with audio bit rate
*
* @var int
*/
protected $bitRate;
/**
* Movie video stream bit rate
*
* @var int
*/
protected $videoBitRate;
/**
* Movie audio stream bit rate
*
* @var int
*/
protected $audioBitRate;
/**
* Audio sample rate
*
* @var int
*/
protected $audioSampleRate;
/**
* Current frame number
*
* @var int
*/
protected $frameNumber;
/**
* Movie video cocec
*
* @var string
*/
protected $videoCodec;
/**
* Movie audio coded
*
* @var string
*/
protected $audioCodec;
/**
* Movie audio channels
*
* @var int
*/
protected $audioChannels;
/**
* Open a video or audio file and return it as an FFmpegMovie object.
* If ffmpeg and ffprobe are both installed on host system, ffmpeg
* gets priority in extracting info from the movie file. However
* to override this default behaviour use any implementation of OutputProvider interface
* as the second constructor argumentwhile instantiating
*
*
* @param string $moviePath full path to the movie file
* @param OutputProvider $outputProvider provides parsable output
* @param string $ffmpegBinary ffmpeg executable, if $outputProvider not specified
* @throws Exception
* @return FFmpegMovie
*/
public function __construct($moviePath, OutputProvider $outputProvider = null, $ffmpegBinary = 'ffmpeg') {
$this->movieFile = $moviePath;
$this->frameNumber = 0;
$this->ffmpegBinary = $ffmpegBinary;
if ($outputProvider === null) {
$outputProvider = new FFmpegOutputProvider($ffmpegBinary);
}
$this->setProvider($outputProvider);
}
/**
* Setting provider implementation
*
* @param OutputProvider $outputProvider
*/
public function setProvider(OutputProvider $outputProvider) {
$this->provider = $outputProvider;
$this->provider->setMovieFile($this->movieFile);
$this->output = $this->provider->getOutput();
}
/**
* Getting current provider implementation
*
* @return OutputProvider
*/
public function getProvider() {
return $this->provider;
}
/**
* Return the duration of a movie or audio file in seconds.
*
* @return float movie duration in seconds
*/
public function getDuration() {
if ($this->duration === null) {
$match = array();
preg_match(self::$REGEX_DURATION, $this->output, $match);
if (array_key_exists(1, $match) && array_key_exists(2, $match) && array_key_exists(3, $match)) {
$hours = (int) $match[1];
$minutes = (int) $match[2];
$seconds = (int) $match[3];
$fractions = (float) ((array_key_exists(5, $match)) ? "0.$match[5]" : 0.0);
$this->duration = (($hours * (3600)) + ($minutes * 60) + $seconds + $fractions);
} else {
$this->duration = 0.0;
}
return $this->duration;
}
return $this->duration;
}
/**
* Return the number of frames in a movie or audio file.
*
* @return int
*/
public function getFrameCount() {
if ($this->frameCount === null) {
$this->frameCount = (int) ($this->getDuration() * $this->getFrameRate());
}
return $this->frameCount;
}
/**
* Return the frame rate of a movie in fps.
*
* @return float
*/
public function getFrameRate() {
if ($this->frameRate === null) {
$match = array();
preg_match(self::$REGEX_FRAME_RATE, $this->output, $match);
$this->frameRate = (float) ((array_key_exists(1, $match)) ? $match[1] : 0.0);
}
return $this->frameRate;
}
/**
* Return the path and name of the movie file or audio file.
*
* @return string
*/
public function getFilename() {
return $this->movieFile;
}
/**
* Return the comment field from the movie or audio file.
*
* @return string
*/
public function getComment() {
if ($this->comment === null) {
$match = array();
preg_match(self::$REGEX_COMMENT, $this->output, $match);
$this->comment = (array_key_exists(2, $match)) ? trim($match[2]) : '';
}
return $this->comment;
}
/**
* Return the title field from the movie or audio file.
*
* @return string
*/
public function getTitle() {
if ($this->title === null) {
$match = array();
preg_match(self::$REGEX_TITLE, $this->output, $match);
$this->title = (array_key_exists(2, $match)) ? trim($match[2]) : '';
}
return $this->title;
}
/**
* Return the author field from the movie or the artist ID3 field from an mp3 file; alias $movie->getArtist()
*
* @return string
*/
public function getArtist() {
if ($this->artist === null) {
$match = array();
preg_match(self::$REGEX_ARTIST, $this->output, $match);
$this->artist = (array_key_exists(3, $match)) ? trim($match[3]) : '';
}
return $this->artist;
}
/**
* Return the author field from the movie or the artist ID3 field from an mp3 file.
*
* @return string
*/
public function getAuthor() {
return $this->getArtist();
}
/**
* Return the copyright field from the movie or audio file.
*
* @return string
*/
public function getCopyright() {
if ($this->copyright === null) {
$match = array();
preg_match(self::$REGEX_COPYRIGHT, $this->output, $match);
$this->copyright = (array_key_exists(2, $match)) ? trim($match[2]) : '';
}
return $this->copyright;
}
/**
* Return the genre ID3 field from an mp3 file.
*
* @return string
*/
public function getGenre() {
if ($this->genre === null) {
$match = array();
preg_match(self::$REGEX_GENRE, $this->output, $match);
$this->genre = (array_key_exists(2, $match)) ? trim($match[2]) : '';
}
return $this->genre;
}
/**
* Return the track ID3 field from an mp3 file.
*
* @return int
*/
public function getTrackNumber() {
if ($this->trackNumber === null) {
$match = array();
preg_match(self::$REGEX_TRACK_NUMBER, $this->output, $match);
$this->trackNumber = (int) ((array_key_exists(2, $match)) ? $match[2] : 0);
}
return $this->trackNumber;
}
/**
* Return the year ID3 field from an mp3 file.
*
* @return int
*/
public function getYear() {
if ($this->year === null) {
$match = array();
preg_match(self::$REGEX_YEAR, $this->output, $match);
$this->year = (int) ((array_key_exists(2, $match)) ? $match[2] : 0);
}
return $this->year;
}
/**
* Return the height of the movie in pixels.
*
* @return int
*/
public function getFrameHeight() {
if ($this->frameHeight == null) {
$match = array();
preg_match(self::$REGEX_FRAME_WH, $this->output, $match);
if (array_key_exists(1, $match) && array_key_exists(2, $match)) {
$this->frameWidth = (int) $match[1];
$this->frameHeight = (int) $match[2];
} else {
$this->frameWidth = 0;
$this->frameHeight = 0;
}
}
return $this->frameHeight;
}
/**
* Return the width of the movie in pixels.
*
* @return int
*/
public function getFrameWidth() {
if ($this->frameWidth === null) {
$this->getFrameHeight();
}
return $this->frameWidth;
}
/**
* Return the pixel format of the movie.
*
* @return string
*/
public function getPixelFormat() {
if ($this->pixelFormat === null) {
$match = array();
preg_match(self::$REGEX_PIXEL_FORMAT, $this->output, $match);
$this->pixelFormat = (array_key_exists(1, $match)) ? trim($match[1]) : '';
}
return $this->pixelFormat;
}
/**
* Return the bit rate of the movie or audio file in bits per second.
*
* @return int
*/
public function getBitRate() {
if ($this->bitRate === null) {
$match = array();
preg_match(self::$REGEX_BITRATE, $this->output, $match);
$this->bitRate = (int) ((array_key_exists(1, $match)) ? ($match[1] * 1000) : 0);
}
return $this->bitRate;
}
/**
* Return the bit rate of the video in bits per second.
*
* NOTE: This only works for files with constant bit rate.
*
* @return int
*/
public function getVideoBitRate() {
if ($this->videoBitRate === null) {
$match = array();
preg_match(self::$REGEX_VIDEO_BITRATE, $this->output, $match);
$this->videoBitRate = (int) ((array_key_exists(1, $match)) ? ($match[1] * 1000) : 0);
}
return $this->videoBitRate;
}
/**
* Return the audio bit rate of the media file in bits per second.
*
* @return int
*/
public function getAudioBitRate() {
if ($this->audioBitRate === null) {
$match = array();
preg_match(self::$REGEX_AUDIO_BITRATE, $this->output, $match);
$this->audioBitRate = (int) ((array_key_exists(1, $match)) ? ($match[1] * 1000) : 0);
}
return $this->audioBitRate;
}
/**
* Return the audio sample rate of the media file in bits per second.
*
* @return int
*/
public function getAudioSampleRate() {
if ($this->audioSampleRate === null) {
$match = array();
preg_match(self::$REGEX_AUDIO_SAMPLE_RATE, $this->output, $match);
$this->audioSampleRate = (int) ((array_key_exists(1, $match)) ? $match[1] : 0);
}
return $this->audioSampleRate;
}
/**
* Return the current frame index.
*
* @return int
*/
public function getFrameNumber() {
return ($this->frameNumber == 0) ? 1 : $this->frameNumber;
}
/**
* Return the name of the video codec used to encode this movie as a string.
*
* @return string
*/
public function getVideoCodec() {
if ($this->videoCodec === null) {
$match = array();
preg_match(self::$REGEX_VIDEO_CODEC, $this->output, $match);
$this->videoCodec = (array_key_exists(1, $match)) ? trim($match[1]) : '';
}
return $this->videoCodec;
}
/**
* Return the name of the audio codec used to encode this movie as a string.
*
* @return string
*/
public function getAudioCodec() {
if ($this->audioCodec === null) {
$match = array();
preg_match(self::$REGEX_AUDIO_CODEC, $this->output, $match);
$this->audioCodec = (array_key_exists(1, $match)) ? trim($match[1]) : '';
}
return $this->audioCodec;
}
/**
* Return the number of audio channels in this movie as an integer.
*
* @return int
*/
public function getAudioChannels() {
if ($this->audioChannels === null) {
$match = array();
preg_match(self::$REGEX_AUDIO_CHANNELS, $this->output, $match);
if (array_key_exists(1, $match)) {
switch (trim($match[1])) {
case 'mono':
$this->audioChannels = 1; break;
case 'stereo':
$this->audioChannels = 2; break;
case '5.1':
$this->audioChannels = 6; break;
case '5:1':
$this->audioChannels = 6; break;
default:
$this->audioChannels = (int) $match[1];
}
} else {
$this->audioChannels = 0;
}
}
return $this->audioChannels;
}
/**
* Return boolean value indicating whether the movie has an audio stream.
*
* @return boolean
*/
public function hasAudio() {
return (boolean) preg_match(self::$REGEX_HAS_AUDIO, $this->output);
}
/**
* Return boolean value indicating whether the movie has a video stream.
*
* @return boolean
*/
public function hasVideo() {
return (boolean) preg_match(self::$REGEX_HAS_VIDEO, $this->output);
}
/**
* Returns a frame from the movie as an FFmpegFrame object. Returns false if the frame was not found.
*
* * framenumber - Frame from the movie to return. If no framenumber is specified, returns the next frame of the movie.
*
* @param int $framenumber
* @param int $height
* @param int $width
* @param int $quality
* @return FFmpegFrame|boolean
*/
public function getFrame($framenumber = null, $height = null, $width = null, $quality = null) {
$framePos = ($framenumber === null) ? $this->frameNumber : (((int) $framenumber) - 1);
// Frame position out of range
if (!is_numeric($framePos) || $framePos < 0 || $framePos > $this->getFrameCount()) {
return false;
}
$frameTime = round((($framePos / $this->getFrameCount()) * $this->getDuration()), 4);
$frame = $this->getFrameAtTime($frameTime, $height, $width, $quality);
// Increment internal frame number
if ($framenumber === null) {
++$this->frameNumber;
}
return $frame;
}
/**
* Returns a frame from the movie as an FFmpegFrame object. Returns false if the frame was not found.
*
* @param float $seconds
* @param int $width
* @param int $height
* @param int $quality
* @param string $frameFilePath
* @param array $output
*
* @throws Exception
*
* @return FFmpegFrame|boolean
*
*/
public function getFrameAtTime($seconds = null, $width = null, $height = null, $quality = null, $frameFilePath = null, &$output = null) {
// Set frame position for frame extraction
$frameTime = ($seconds === null) ? 0 : $seconds;
// time out of range
if (!is_numeric($frameTime) || $frameTime < 0 || $frameTime > $this->getDuration()) {
throw(new Exception('Frame time is not in range '.$frameTime.'/'.$this->getDuration().' '.$this->getFilename()));
}
if(is_numeric($height) && is_numeric($width)) {
$image_size = ' -s '.$width.'x'.$height;
} else {
$image_size = '';
}
if(is_numeric($quality)) {
$quality = ' -qscale '.$quality;
} else {
$quality = '';
}
$deleteTmp = false;
if ($frameFilePath === null) {
$frameFilePath = sys_get_temp_dir().DIRECTORY_SEPARATOR.uniqid('frame', true).'.jpg';
$deleteTmp = true;
}
$output = array();
// Fast and accurate way to seek. First quick-seek before input up to
// a point just before the frame, and then accurately seek after input
// to the exact point.
// See: http://ffmpeg.org/trac/ffmpeg/wiki/Seeking%20with%20FFmpeg
if ($frameTime > 30) {
$seek1 = $frameTime - 30;
$seek2 = 30;
} else {
$seek1 = 0;
$seek2 = $frameTime;
}
exec(implode(' ', array(
$this->ffmpegBinary,
'-ss '.$seek1,
'-i '.escapeshellarg($this->movieFile),
'-f image2',
'-ss '.$seek2,
'-vframes 1',
$image_size,
$quality,
escapeshellarg($frameFilePath),
'2>&1',
)), $output, $retVar);
$output = join(PHP_EOL, $output);
// Cannot write frame to the data storage
if (!file_exists($frameFilePath)) {
// Find error in output
preg_match(self::$REGEX_ERRORS, $output, $errors);
if ($errors) {
throw new Exception($errors[0]);
}
// Default file not found error
throw new Exception('TMP image not found/written '. $frameFilePath);
}
// Create gdimage and delete temporary image
$gdImage = imagecreatefromjpeg($frameFilePath);
if ($deleteTmp && is_writable($frameFilePath)) {
unlink($frameFilePath);
}
$frame = new FFmpegFrame($gdImage, $frameTime);
imagedestroy($gdImage);
return $frame;
}
/**
* Returns the next key frame from the movie as an FFmpegFrame object. Returns false if the frame was not found.
*
* @return FFmpegFrame|boolean
*/
public function getNextKeyFrame() {
return $this->getFrame();
}
public function __clone() {
$this->provider = clone $this->provider;
}
public function serialize() {
$data = serialize(array(
$this->ffmpegBinary,
$this->movieFile,
$this->output,
$this->frameNumber,
$this->provider
));
return $data;
}
public function unserialize($serialized) {
list($this->ffmpegBinary,
$this->movieFile,
$this->output,
$this->frameNumber,
$this->provider
) = unserialize($serialized);
}
}