diff options
Diffstat (limited to 'extensions/TimedMediaHandler/handlers')
17 files changed, 3701 insertions, 0 deletions
diff --git a/extensions/TimedMediaHandler/handlers/FLACHandler/FLACHandler.php b/extensions/TimedMediaHandler/handlers/FLACHandler/FLACHandler.php new file mode 100644 index 00000000..f08c9b90 --- /dev/null +++ b/extensions/TimedMediaHandler/handlers/FLACHandler/FLACHandler.php @@ -0,0 +1,74 @@ +<?php +/** + * FLAC handler + */ +class FLACHandler extends ID3Handler { + + /** + * @param $file File + * @return string + */ + function getMetadataType( $file ) { + return 'flac'; + } + + /** + * @param $file File + * @return String + */ + function getWebType( $file ) { + return 'audio/flac'; + } + + /** + * @param $file File + * @return array|bool + */ + function getStreamTypes( $file ) { + $streamTypes = array(); + $metadata = $this->unpackMetadata( $file->getMetadata() ); + + if ( !$metadata || isset( $metadata['error'] ) ) { + return false; + } + + if( isset( $metadata['audio'] ) && $metadata['audio']['dataformat'] == 'flac' ){ + $streamTypes[] = 'FLAC'; + } + + return $streamTypes; + } + + /** + * @param $file File + * @return String + */ + function getShortDesc( $file ) { + global $wgLang; + + $streamTypes = $this->getStreamTypes( $file ); + if ( !$streamTypes ) { + return parent::getShortDesc( $file ); + } + return wfMessage( 'timedmedia-flac-short-audio', + $wgLang->formatTimePeriod( $this->getLength( $file ) ) )->text(); + } + + /** + * @param $file File + * @return String + */ + function getLongDesc( $file ) { + global $wgLang; + $streamTypes = $this->getStreamTypes( $file ); + if ( !$streamTypes ) { + return parent::getLongDesc( $file ); + } + return wfMessage('timedmedia-flac-long-audio', + $wgLang->formatTimePeriod( $this->getLength($file) ), + $wgLang->formatBitrate( $this->getBitRate( $file ) ) + )->text(); + + } + +} diff --git a/extensions/TimedMediaHandler/handlers/ID3Handler/ID3Handler.php b/extensions/TimedMediaHandler/handlers/ID3Handler/ID3Handler.php new file mode 100644 index 00000000..1e3ae4fb --- /dev/null +++ b/extensions/TimedMediaHandler/handlers/ID3Handler/ID3Handler.php @@ -0,0 +1,107 @@ +<?php +/** + * getID3 Metadata handler + */ +class ID3Handler extends TimedMediaHandler { + // XXX match GETID3_VERSION ( too bad version is not a getter ) + const METADATA_VERSION = 2; + + /** + * @param $path string + * @return array + */ + protected function getID3( $path ) { + // Create new id3 object: + $getID3 = new getID3(); + + // Don't grab stuff we don't use: + $getID3->option_tag_id3v1 = false; // Read and process ID3v1 tags + $getID3->option_tag_id3v2 = false; // Read and process ID3v2 tags + $getID3->option_tag_lyrics3 = false; // Read and process Lyrics3 tags + $getID3->option_tag_apetag = false; // Read and process APE tags + $getID3->option_tags_process = false; // Copy tags to root key 'tags' and encode to $this->encoding + $getID3->option_tags_html = false; // Copy tags to root key 'tags_html' properly translated from various encodings to HTML entities + + // Analyze file to get metadata structure: + $id3 = $getID3->analyze( $path ); + + // remove file paths + unset( $id3['filename'] ); + unset( $id3['filepath'] ); + unset( $id3['filenamepath']); + + // Update the version + $id3['version'] = self::METADATA_VERSION; + + return $id3; + } + + /** + * @param $file File + * @param $path string + * @return string + */ + function getMetadata( $file, $path ) { + $id3 = $this->getID3( $path ); + return serialize( $id3 ); + } + + /** + * @param $metadata + * @return bool|mixed + */ + function unpackMetadata( $metadata ) { + wfSuppressWarnings(); + $unser = unserialize( $metadata ); + wfRestoreWarnings(); + if ( isset( $unser['version'] ) && $unser['version'] == self::METADATA_VERSION ) { + return $unser; + } else { + return false; + } + } + + /** + * @param $file File + * @return mixed + */ + function getBitrate( $file ){ + $metadata = $this->unpackMetadata( $file->getMetadata() ); + if ( !$metadata || isset( $metadata['error'] ) || !isset( $metadata['bitrate'] ) ) { + return 0; + } else { + return $metadata['bitrate']; + } + } + + /** + * @param $file File + * @return int + */ + function getLength( $file ) { + $metadata = $this->unpackMetadata( $file->getMetadata() ); + if ( !$metadata || isset( $metadata['error'] ) || !isset( $metadata['playtime_seconds'] ) ) { + return 0; + } else { + return $metadata['playtime_seconds']; + } + } + + /** + * @param $file File + * @return bool|int + */ + function getFramerate( $file ){ + $metadata = $this->unpackMetadata( $file->getMetadata() ); + if ( !$metadata || isset( $metadata['error'] ) ) { + return 0; + } else { + // return the frame rate of the first found video stream: + if( isset( $metadata['video'] ) + && isset( $metadata['video']['frame_rate'] ) ) { + return $metadata['video']['frame_rate']; + } + return false; + } + } +} diff --git a/extensions/TimedMediaHandler/handlers/Mp4Handler/Mp4Handler.php b/extensions/TimedMediaHandler/handlers/Mp4Handler/Mp4Handler.php new file mode 100644 index 00000000..897e1017 --- /dev/null +++ b/extensions/TimedMediaHandler/handlers/Mp4Handler/Mp4Handler.php @@ -0,0 +1,139 @@ +<?php +/** + * MP4 handler + */ +class Mp4Handler extends ID3Handler { + + /** + * @param $path string + * @return array + */ + protected function getID3( $path ) { + $id3 = parent::getID3( $path ); + // Unset some parts of id3 that are too detailed and matroska specific: + unset( $id3['quicktime'] ); + return $id3; + } + + /** + * Get the "media size" + * @param $file File + * @param $path string + * @param $metadata bool + * @return array|bool + */ + function getImageSize( $file, $path, $metadata = false ) { + // Just return the size of the first video stream + if ( $metadata === false ) { + $metadata = $file->getMetadata(); + } + $metadata = $this->unpackMetadata( $metadata ); + if ( isset( $metadata['error'] ) ) { + return false; + } + if( isset( $metadata['video']['resolution_x']) + && + isset( $metadata['video']['resolution_y']) + ){ + return array ( + $metadata['video']['resolution_x'], + $metadata['video']['resolution_y'] + ); + } + return array( false, false ); + } + + /** + * @param $image + * @return string + */ + function getMetadataType( $image ) { + return 'mp4'; + } + /** + * @param $file File + */ + function getWebType( $file ) { + /** + * h.264 profile types: + H.264 Simple baseline profile video (main and extended video compatible) level 3 and Low-Complexity AAC audio in MP4 container: + type='video/mp4; codecs="avc1.42E01E, mp4a.40.2"' + + H.264 Extended profile video (baseline-compatible) level 3 and Low-Complexity AAC audio in MP4 container: + type='video/mp4; codecs="avc1.58A01E, mp4a.40.2"' + + H.264 Main profile video level 3 and Low-Complexity AAC audio in MP4 container + type='video/mp4; codecs="avc1.4D401E, mp4a.40.2"' + + H.264 ‘High’ profile video (incompatible with main, baseline, or extended profiles) level 3 and Low-Complexity AAC audio in MP4 container + type='video/mp4; codecs="avc1.64001E, mp4a.40.2"' + */ + // all h.264 encodes are currently simple profile + return 'video/mp4; codecs="avc1.42E01E, mp4a.40.2"'; + } + /** + * @param $file File + * @return array|bool + */ + function getStreamTypes( $file ) { + $streamTypes = array(); + $metadata = self::unpackMetadata( $file->getMetadata() ); + if ( !$metadata || isset( $metadata['error'] ) ) { + return false; + } + if( isset( $metadata['audio'] ) && $metadata['audio']['dataformat'] == 'mp4' ){ + if( isset( $metadata['audio']['codec'] ) + && + strpos( $metadata['audio']['codec'] , 'AAC' ) !== false + ){ + $streamTypes[] = 'AAC'; + } else { + $streamTypes[] = $metadata['audio']['codec']; + } + } + // id3 gives 'V_VP8' for what we call VP8 + if( $metadata['video']['dataformat'] == 'quicktime' ){ + $streamTypes[] = 'h.264'; + } + + return $streamTypes; + } + + /** + * @param $file File + * @return String + */ + function getShortDesc( $file ) { + $streamTypes = $this->getStreamTypes( $file ); + if ( !$streamTypes ) { + return parent::getShortDesc( $file ); + } + return wfMessage( 'timedmedia-mp4-short-video', implode( '/', $streamTypes ) + )->timeperiodParams( + $this->getLength( $file ) + )->text(); + } + + /** + * @param $file File + * @return String + */ + function getLongDesc( $file ) { + $streamTypes = $this->getStreamTypes( $file ); + if ( !$streamTypes ) { + return parent::getLongDesc( $file ); + } + return wfMessage('timedmedia-mp4-long-video', + implode( '/', $streamTypes ) + )->timeperiodParams( + $this->getLength( $file ) + )->bitrateParams( + $this->getBitRate( $file ) + )->numParams( + $file->getWidth(), + $file->getHeight() + )->text(); + + } + +} diff --git a/extensions/TimedMediaHandler/handlers/OggHandler/File_Ogg/File/Ogg.php b/extensions/TimedMediaHandler/handlers/OggHandler/File_Ogg/File/Ogg.php new file mode 100644 index 00000000..ca281e22 --- /dev/null +++ b/extensions/TimedMediaHandler/handlers/OggHandler/File_Ogg/File/Ogg.php @@ -0,0 +1,631 @@ +<?php +/* vim: set expandtab tabstop=4 shiftwidth=4: */ +// +----------------------------------------------------------------------------+ +// | File_Ogg PEAR Package for Accessing Ogg Bitstreams | +// | Copyright (c) 2005-2007 | +// | David Grant <david@grant.org.uk> | +// | Tim Starling <tstarling@wikimedia.org> | +// +----------------------------------------------------------------------------+ +// | This library is free software; you can redistribute it and/or | +// | modify it under the terms of the GNU Lesser General Public | +// | License as published by the Free Software Foundation; either | +// | version 2.1 of the License, or (at your option) any later version. | +// | | +// | This library is distributed in the hope that it will be useful, | +// | but WITHOUT ANY WARRANTY; without even the implied warranty of | +// | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | +// | Lesser General Public License for more details. | +// | | +// | You should have received a copy of the GNU Lesser General Public | +// | License along with this library; if not, write to the Free Software | +// | Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA | +// +----------------------------------------------------------------------------+ + +/** + * @author David Grant <david@grant.org.uk>, Tim Starling <tstarling@wikimedia.org> + * @category File + * @copyright David Grant <david@grant.org.uk>, Tim Starling <tstarling@wikimedia.org> + * @license http://www.gnu.org/copyleft/lesser.html GNU LGPL + * @link http://pear.php.net/package/File_Ogg + * @package File_Ogg + * @version CVS: $Id: Ogg.php,v 1.14 2005/11/19 09:06:30 djg Exp $ + */ + +/** + * @access public + */ +define("OGG_STREAM_VORBIS", 1); +/** + * @access public + */ +define("OGG_STREAM_THEORA", 2); +/** + * @access public + */ +define("OGG_STREAM_SPEEX", 3); +/** + * @access public + */ +define("OGG_STREAM_FLAC", 4); +/** + * @access public + */ +define("OGG_STREAM_OPUS", 5); + +/** + * Capture pattern to determine if a file is an Ogg physical stream. + * + * @access private + */ +define("OGG_CAPTURE_PATTERN", "OggS"); +/** + * Maximum size of an Ogg stream page plus four. This value is specified to allow + * efficient parsing of the physical stream. The extra four is a paranoid measure + * to make sure a capture pattern is not split into two parts accidentally. + * + * @access private + */ +define("OGG_MAXIMUM_PAGE_SIZE", 65311); +/** + * Capture pattern for an Ogg Vorbis logical stream. + * + * @access private + */ +define("OGG_STREAM_CAPTURE_VORBIS", "vorbis"); +/** + * Capture pattern for an Ogg Speex logical stream. + * @access private + */ +define("OGG_STREAM_CAPTURE_SPEEX", "Speex "); +/** + * Capture pattern for an Ogg FLAC logical stream. + * + * @access private + */ +define("OGG_STREAM_CAPTURE_FLAC", "FLAC"); +/** + * Capture pattern for an Ogg Theora logical stream. + * + * @access private + */ +define("OGG_STREAM_CAPTURE_THEORA", "theora"); +/** + * Capture pattern for an Ogg Opus logical stream. + * @access private + */ +define("OGG_STREAM_CAPTURE_OPUS", "OpusHead"); +/** + * Error thrown if the file location passed is nonexistant or unreadable. + * + * @access private + */ +define("OGG_ERROR_INVALID_FILE", 1); +/** + * Error thrown if the user attempts to extract an unsupported logical stream. + * + * @access private + */ +define("OGG_ERROR_UNSUPPORTED", 2); +/** + * Error thrown if the user attempts to extract an logical stream with no + * corresponding serial number. + * + * @access private + */ +define("OGG_ERROR_BAD_SERIAL", 3); +/** + * Error thrown if the stream appears to be corrupted. + * + * @access private + */ +define("OGG_ERROR_UNDECODABLE", 4); + + +/** + * Class for parsing a ogg bitstream. + * + * This class provides a means to access several types of logical bitstreams (e.g. Vorbis) + * within a Ogg physical bitstream. + * + * @link http://www.xiph.org/ogg/doc/ + * @package File_Ogg + */ +class File_Ogg +{ + /** + * File pointer to Ogg container. + * + * This is the file pointer used for extracting data from the Ogg stream. It is + * the result of a standard fopen call. + * + * @var pointer + * @access private + */ + var $_filePointer; + + /** + * The container for all logical streams. + * + * List of all of the unique streams in the Ogg physical stream. The key + * used is the unique serial number assigned to the logical stream by the + * encoding application. + * + * @var array + * @access private + */ + var $_streamList = array(); + var $_streams = array(); + + /** + * Length in seconds of each stream group + */ + var $_groupLengths = array(); + + /** + * Total length in seconds of the entire file + */ + var $_totalLength; + var $_startOffset = false; + + /** + * Maximum number of pages to store detailed metadata for, per stream. + * We can't store every page because there could be millions, causing an OOM. + * This must be big enough so that all the codecs can get the metadata they + * need without re-reading the file. + */ + var $_maxPageCacheSize = 4; + + /** + * Returns an interface to an Ogg physical stream. + * + * This method takes the path to a local file and examines it for a physical + * ogg bitsream. After instantiation, the user should query the object for + * the logical bitstreams held within the ogg container. + * + * @access public + * @param string $fileLocation The path of the file to be examined. + */ + function __construct($fileLocation) + { + clearstatcache(); + if (! file_exists($fileLocation)) { + throw new OggException("Couldn't Open File. Check File Path.", OGG_ERROR_INVALID_FILE); + } + + // Open this file as a binary, and split the file into streams. + $this->_filePointer = fopen($fileLocation, "rb"); + if (!is_resource($this->_filePointer)) + throw new OggException("Couldn't Open File. Check File Permissions.", OGG_ERROR_INVALID_FILE); + + // Check for a stream at the start + $magic = fread($this->_filePointer, strlen(OGG_CAPTURE_PATTERN)); + if ($magic !== OGG_CAPTURE_PATTERN) { + throw new OggException("Couldn't read file: Incorrect magic number.", OGG_ERROR_UNDECODABLE); + } + fseek($this->_filePointer, 0, SEEK_SET); + + $this->_splitStreams(); + fclose($this->_filePointer); + } + + /** + * Little-endian equivalent for bin2hex + * @static + */ + static function _littleEndianBin2Hex( $bin ) { + $bigEndian = bin2hex( $bin ); + // Reverse entire string + $reversed = strrev( $bigEndian ); + // Swap nibbles back + for ( $i = 0; $i < strlen( $bigEndian ); $i += 2 ) { + $temp = $reversed[$i]; + $reversed[$i] = $reversed[$i+1]; + $reversed[$i+1] = $temp; + } + return $reversed; + } + + + /** + * Read a binary structure from a file. An array of unsigned integers are read. + * Large integers are upgraded to floating point on overflow. + * + * Format is big-endian as per Theora bit packing convention, this function + * won't work for Vorbis. + * + * @param resource $file + * @param array $fields Associative array mapping name to length in bits + */ + static function _readBigEndian($file, $fields) + { + $bufferLength = ceil(array_sum($fields) / 8); + $buffer = fread($file, $bufferLength); + if (strlen($buffer) != $bufferLength) { + throw new OggException('Unexpected end of file', OGG_ERROR_UNDECODABLE); + } + $bytePos = 0; + $bitPos = 0; + $byteValue = ord($buffer[0]); + $output = array(); + foreach ($fields as $name => $width) { + if ($width % 8 == 0 && $bitPos == 0) { + // Byte aligned case + $bytes = $width / 8; + $endBytePos = $bytePos + $bytes; + $value = 0; + while ($bytePos < $endBytePos) { + $value = ($value * 256) + ord($buffer[$bytePos]); + $bytePos++; + } + if ($bytePos < strlen($buffer)) { + $byteValue = ord($buffer[$bytePos]); + } + } else { + // General case + $bitsRemaining = $width; + $value = 0; + while ($bitsRemaining > 0) { + $bitsToRead = min($bitsRemaining, 8 - $bitPos); + $byteValue <<= $bitsToRead; + $overflow = ($byteValue & 0xff00) >> 8; + $byteValue &= $byteValue & 0xff; + + $bitPos += $bitsToRead; + $bitsRemaining -= $bitsToRead; + $value += $overflow * pow(2, $bitsRemaining); + + if ($bitPos >= 8) { + $bitPos = 0; + $bytePos++; + if ($bitsRemaining <= 0) { + break; + } + $byteValue = ord($buffer[$bytePos]); + } + } + } + $output[$name] = $value; + assert($bytePos <= $bufferLength); + } + return $output; + } + + /** + * Read a binary structure from a file. An array of unsigned integers are read. + * Large integers are upgraded to floating point on overflow. + * + * Format is little-endian as per Vorbis bit packing convention. + * + * @param resource $file + * @param array $fields Associative array mapping name to length in bits + */ + static function _readLittleEndian( $file, $fields ) { + $bufferLength = ceil(array_sum($fields) / 8); + $buffer = fread($file, $bufferLength); + if (strlen($buffer) != $bufferLength) { + throw new OggException('Unexpected end of file', OGG_ERROR_UNDECODABLE); + } + + $bytePos = 0; + $bitPos = 0; + $byteValue = ord($buffer[0]) << 8; + $output = array(); + foreach ($fields as $name => $width) { + if ($width % 8 == 0 && $bitPos == 0) { + // Byte aligned case + $bytes = $width / 8; + $value = 0; + for ($i = 0; $i < $bytes; $i++, $bytePos++) { + $value += pow(256, $i) * ord($buffer[$bytePos]); + } + if ($bytePos < strlen($buffer)) { + $byteValue = ord($buffer[$bytePos]) << 8; + } + } else { + // General case + $bitsRemaining = $width; + $value = 0; + while ($bitsRemaining > 0) { + $bitsToRead = min($bitsRemaining, 8 - $bitPos); + $byteValue >>= $bitsToRead; + $overflow = ($byteValue & 0xff) >> (8 - $bitsToRead); + $byteValue &= 0xff00; + + $value += $overflow * pow(2, $width - $bitsRemaining); + $bitPos += $bitsToRead; + $bitsRemaining -= $bitsToRead; + + if ($bitPos >= 8) { + $bitPos = 0; + $bytePos++; + if ($bitsRemaining <= 0) { + break; + } + $byteValue = ord($buffer[$bytePos]) << 8; + } + } + } + $output[$name] = $value; + assert($bytePos <= $bufferLength); + } + return $output; + } + + + /** + * @access private + */ + function _decodePageHeader($pageData, $pageOffset, $groupId) + { + // Extract the various bits and pieces found in each packet header. + if (substr($pageData, 0, 4) != OGG_CAPTURE_PATTERN) + return (false); + + $stream_version = unpack("C1data", substr($pageData, 4, 1)); + if ($stream_version['data'] != 0x00) + return (false); + + $header_flag = unpack("Cdata", substr($pageData, 5, 1)); + + // Exact granule position + $abs_granule_pos = self::_littleEndianBin2Hex( substr($pageData, 6, 8)); + + // Approximate (floating point) granule position + $pos = unpack("Va/Vb", substr($pageData, 6, 8)); + $approx_granule_pos = $pos['a'] + $pos['b'] * pow(2, 32); + + // Serial number for the current datastream. + $stream_serial = unpack("Vdata", substr($pageData, 14, 4)); + $page_sequence = unpack("Vdata", substr($pageData, 18, 4)); + $checksum = unpack("Vdata", substr($pageData, 22, 4)); + $page_segments = unpack("Cdata", substr($pageData, 26, 1)); + $segments_total = 0; + for ($i = 0; $i < $page_segments['data']; ++$i) { + $segment_length = unpack("Cdata", substr($pageData, 26 + ($i + 1), 1)); + $segments_total += $segment_length['data']; + } + $pageFinish = $pageOffset + 27 + $page_segments['data'] + $segments_total; + $page = array( + 'stream_version' => $stream_version['data'], + 'header_flag' => $header_flag['data'], + 'abs_granule_pos' => $abs_granule_pos, + 'approx_granule_pos' => $approx_granule_pos, + 'checksum' => sprintf("%u", $checksum['data']), + 'segments' => $page_segments['data'], + 'head_offset' => $pageOffset, + 'body_offset' => $pageOffset + 27 + $page_segments['data'], + 'body_finish' => $pageFinish, + 'data_length' => $pageFinish - $pageOffset, + 'group' => $groupId, + ); + if ( !isset( $this->_streamList[$stream_serial['data']] ) ) { + $this->_streamList[$stream_serial['data']] = array( + 'pages' => array(), + 'data_length' => 0, + 'first_granule_pos' => null, + 'last_granule_pos' => null, + ); + } + $stream =& $this->_streamList[$stream_serial['data']]; + if ( count( $stream['pages'] ) < $this->_maxPageCacheSize ) { + $stream['pages'][$page_sequence['data']] = $page; + } + $stream['last_page'] = $page; + $stream['data_length'] += $page['data_length']; + + # Reject -1 as a granule pos, that means no segment finished in the packet + if ( $abs_granule_pos !== 'ffffffffffffffff' ) { + if ( $stream['first_granule_pos'] === null ) { + $stream['first_granule_pos'] = $abs_granule_pos; + } + $stream['last_granule_pos'] = $abs_granule_pos; + } + + $pageData = null; + return $page; + } + + /** + * @access private + */ + function _splitStreams() + { + // Loop through the physical stream until there are no more pages to read. + $groupId = 0; + $openStreams = 0; + $this_page_offset = 0; + while (!feof($this->_filePointer)) { + $pageData = fread($this->_filePointer, 282); + if (strval($pageData) === '') { + break; + } + $page = $this->_decodePageHeader($pageData, $this_page_offset, $groupId); + if ($page === false) { + throw new OggException("Cannot decode Ogg file: Invalid page at offset $this_page_offset", OGG_ERROR_UNDECODABLE); + } + + // Keep track of multiplexed groups + if ($page['header_flag'] & 2/*bos*/) { + $openStreams++; + } elseif ($page['header_flag'] & 4/*eos*/) { + $openStreams--; + if (!$openStreams) { + // End of group + $groupId++; + } + } + if ($openStreams < 0) { + throw new OggException("Unexpected end of stream", OGG_ERROR_UNDECODABLE); + } + + $this_page_offset = $page['body_finish']; + fseek( $this->_filePointer, $this_page_offset, SEEK_SET ); + } + // Loop through the streams, and find out what type of stream is available. + $groupLengths = array(); + foreach ($this->_streamList as $stream_serial => $streamData) { + fseek($this->_filePointer, $streamData['pages'][0]['body_offset'], SEEK_SET); + $pattern = fread($this->_filePointer, 8); + if (preg_match("/" . OGG_STREAM_CAPTURE_VORBIS . "/", $pattern)) { + $this->_streamList[$stream_serial]['stream_type'] = OGG_STREAM_VORBIS; + $stream = new File_Ogg_Vorbis($stream_serial, $streamData, $this->_filePointer); + } elseif (preg_match("/" . OGG_STREAM_CAPTURE_SPEEX . "/", $pattern)) { + $this->_streamList[$stream_serial]['stream_type'] = OGG_STREAM_SPEEX; + $stream = new File_Ogg_Speex($stream_serial, $streamData, $this->_filePointer); + } elseif (preg_match("/" . OGG_STREAM_CAPTURE_FLAC . "/", $pattern)) { + $this->_streamList[$stream_serial]['stream_type'] = OGG_STREAM_FLAC; + $stream = new File_Ogg_Flac($stream_serial, $streamData, $this->_filePointer); + } elseif (preg_match("/" . OGG_STREAM_CAPTURE_THEORA . "/", $pattern)) { + $this->_streamList[$stream_serial]['stream_type'] = OGG_STREAM_THEORA; + $stream = new File_Ogg_Theora($stream_serial, $streamData, $this->_filePointer); + } elseif (preg_match("/" . OGG_STREAM_CAPTURE_OPUS . "/", $pattern)) { + $this->_streamList[$stream_serial]['stream_type'] = OGG_STREAM_OPUS; + $stream = new File_Ogg_Opus($stream_serial, $streamData, $this->_filePointer); + } else { + $streamData['stream_type'] = "unknown"; + $stream = false; + } + + if ($stream) { + $this->_streams[$stream_serial] = $stream; + $group = $streamData['pages'][0]['group']; + if (isset($groupLengths[$group])) { + $groupLengths[$group] = max($groupLengths[$group], $stream->getLength()); + } else { + $groupLengths[$group] = $stream->getLength(); + } + //just store the startOffset for the first stream: + if( $this->_startOffset === false ){ + $this->_startOffset = $stream->getStartOffset(); + } + + } + } + $this->_groupLengths = $groupLengths; + $this->_totalLength = array_sum( $groupLengths ); + unset($this->_streamList); + } + + /** + * Returns the overead percentage used by the Ogg headers. + * + * This function returns the percentage of the total stream size + * used for Ogg headers. + * + * @return float + */ + function getOverhead() { + $header_size = 0; + $stream_size = 0; + foreach ($this->_streams as $serial => $stream) { + foreach ($stream->_streamList as $offset => $stream_data) { + $header_size += $stream_data['body_offset'] - $stream_data['head_offset']; + $stream_size = $stream_data['body_finish']; + } + } + return sprintf("%0.2f", ($header_size / $stream_size) * 100); + } + + /** + * Returns the appropriate logical bitstream that corresponds to the provided serial. + * + * This function returns a logical bitstream contained within the Ogg physical + * stream, corresponding to the serial used as the offset for that bitstream. + * The returned stream may be Vorbis, Speex, FLAC or Theora, although the only + * usable bitstream is Vorbis. + * + * @return File_Ogg_Bitstream + */ + function &getStream($streamSerial) + { + if (! array_key_exists($streamSerial, $this->_streams)) + throw new OggException("The stream number is invalid.", OGG_ERROR_BAD_SERIAL); + + return $this->_streams[$streamSerial]; + } + + /** + * This function returns true if a logical bitstream of the requested type can be found. + * + * This function checks the contents of this ogg physical bitstream for of logical + * bitstream corresponding to the supplied type. If one is found, the function returns + * true, otherwise it return false. + * + * @param int $streamType + * @return boolean + */ + function hasStream($streamType) + { + foreach ($this->_streams as $stream) { + if ($stream['stream_type'] == $streamType) + return (true); + } + return (false); + } + + /** + * Returns an array of logical streams inside this physical bitstream. + * + * This function returns an array of logical streams found within this physical + * bitstream. If a filter is provided, only logical streams of the requested type + * are returned, as an array of serial numbers. If no filter is provided, this + * function returns a two-dimensional array, with the stream type as the primary key, + * and a value consisting of an array of stream serial numbers. + * + * @param int $filter + * @return array + */ + function listStreams($filter = null) + { + $streams = array(); + // Loops through the streams and assign them to an appropriate index, + // ready for filtering the second part of this function. + foreach ($this->_streams as $serial => $stream) { + $stream_type = 0; + switch (get_class($stream)) { + case "file_ogg_flac": + $stream_type = OGG_STREAM_FLAC; + break; + case "file_ogg_speex": + $stream_type = OGG_STREAM_SPEEX; + break; + case "file_ogg_theora": + $stream_type = OGG_STREAM_THEORA; + break; + case "file_ogg_vorbis": + $stream_type = OGG_STREAM_VORBIS; + break; + } + if (! isset($streams[$stream_type])) + // Initialise the result list for this stream type. + $streams[$stream_type] = array(); + + $streams[$stream_type][] = $serial; + } + + // Perform filtering. + if (is_null($filter)) + return ($streams); + elseif (isset($streams[$filter])) + return ($streams[$filter]); + else + return array(); + } + /** + * getStartOffset + * + * @return unknown + */ + function getStartOffset(){ + if( $this->_startOffset === false) + return 0; + return $this->_startOffset; + } + /** + * Get the total length of the group of streams + */ + function getLength() { + return $this->_totalLength; + } +} +?> diff --git a/extensions/TimedMediaHandler/handlers/OggHandler/File_Ogg/File/Ogg/Bitstream.php b/extensions/TimedMediaHandler/handlers/OggHandler/File_Ogg/File/Ogg/Bitstream.php new file mode 100644 index 00000000..1a462232 --- /dev/null +++ b/extensions/TimedMediaHandler/handlers/OggHandler/File_Ogg/File/Ogg/Bitstream.php @@ -0,0 +1,125 @@ +<?php +/* vim: set expandtab tabstop=4 shiftwidth=4: */ +// +----------------------------------------------------------------------------+ +// | File_Ogg PEAR Package for Accessing Ogg Bitstreams | +// | Copyright (c) 2005-2007 | +// | David Grant <david@grant.org.uk> | +// | Tim Starling <tstarling@wikimedia.org> | +// +----------------------------------------------------------------------------+ +// | This library is free software; you can redistribute it and/or | +// | modify it under the terms of the GNU Lesser General Public | +// | License as published by the Free Software Foundation; either | +// | version 2.1 of the License, or (at your option) any later version. | +// | | +// | This library is distributed in the hope that it will be useful, | +// | but WITHOUT ANY WARRANTY; without even the implied warranty of | +// | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | +// | Lesser General Public License for more details. | +// | | +// | You should have received a copy of the GNU Lesser General Public | +// | License along with this library; if not, write to the Free Software | +// | Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA | +// +----------------------------------------------------------------------------+ + + +/** + * @author David Grant <david@grant.org.uk>, Tim Starling <tstarling@wikimedia.org> + * @category File + * @copyright David Grant <david@grant.org.uk>, Tim Starling <tstarling@wikimedia.org> + * @license http://www.gnu.org/copyleft/lesser.html GNU LGPL + * @link http://pear.php.net/package/File_Ogg + * @package File_Ogg + * @version CVS: $Id: Bitstream.php,v 1.3 2005/11/08 19:36:18 djg Exp $ + */ +class File_Ogg_Bitstream +{ + /** + * The serial number of this logical stream. + * + * @var int + * @access private + */ + var $_streamSerial; + /** + * @access private + */ + var $_streamData; + /** + * @access private + */ + var $_filePointer; + /** + * The number of bits used in this stream. + * + * @var int + * @access private + */ + var $_streamSize; + + /** + * The last granule position in the stream + * @var int + * @access private + */ + var $_lastGranulePos; + + /** + * Constructor for a generic logical stream. + * + * @param int $streamSerial Serial number of the logical stream. + * @param array $streamData Data for the requested logical stream. + * @param string $filePath Location of a file on the filesystem. + * @param pointer $filePointer File pointer for the current physical stream. + * @access private + */ + function __construct($streamSerial, $streamData, $filePointer) + { + $this->_streamSerial = $streamSerial; + $this->_streamData = $streamData; + $this->_filePointer = $filePointer; + $this->_firstGranulePos = $streamData['first_granule_pos']; + $this->_lastGranulePos = $streamData['last_granule_pos']; + $this->_streamSize = $streamData['data_length']; + $this->_group = $streamData['pages'][0]['group']; + } + + /** + * Gives the serial number of this stream. + * + * The stream serial number is of fairly academic importance, as it makes little + * difference to the end user. The serial number is used by the Ogg physical + * stream to distinguish between concurrent logical streams. + * + * @return int + * @access public + */ + function getSerial() + { + return ($this->_streamSerial); + } + + /** + * Gives the size (in bits) of this stream. + * + * This function returns the size of the Vorbis stream within the Ogg + * physical stream. + * + * @return int + * @access public + */ + function getSize() + { + return ($this->_streamSize); + } + + /** + * Get the multiplexed group ID + */ + function getGroup() + { + return $this->_group; + } + +} + +?> diff --git a/extensions/TimedMediaHandler/handlers/OggHandler/File_Ogg/File/Ogg/Flac.php b/extensions/TimedMediaHandler/handlers/OggHandler/File_Ogg/File/Ogg/Flac.php new file mode 100644 index 00000000..6838ba68 --- /dev/null +++ b/extensions/TimedMediaHandler/handlers/OggHandler/File_Ogg/File/Ogg/Flac.php @@ -0,0 +1,133 @@ +<?php +/* vim: set expandtab tabstop=4 shiftwidth=4: */ +// +----------------------------------------------------------------------------+ +// | File_Ogg PEAR Package for Accessing Ogg Bitstreams | +// | Copyright (c) 2005-2007 | +// | David Grant <david@grant.org.uk> | +// | Tim Starling <tstarling@wikimedia.org> | +// +----------------------------------------------------------------------------+ +// | This library is free software; you can redistribute it and/or | +// | modify it under the terms of the GNU Lesser General Public | +// | License as published by the Free Software Foundation; either | +// | version 2.1 of the License, or (at your option) any later version. | +// | | +// | This library is distributed in the hope that it will be useful, | +// | but WITHOUT ANY WARRANTY; without even the implied warranty of | +// | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | +// | Lesser General Public License for more details. | +// | | +// | You should have received a copy of the GNU Lesser General Public | +// | License along with this library; if not, write to the Free Software | +// | Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA | +// +----------------------------------------------------------------------------+ + + +/** + * @author David Grant <david@grant.org.uk>, Tim Starling <tstarling@wikimedia.org> + * @category File + * @copyright David Grant <david@grant.org.uk>, Tim Starling <tstarling@wikimedia.org> + * @license http://www.gnu.org/copyleft/lesser.html GNU LGPL + * @link http://pear.php.net/package/File_Ogg + * @link http://flac.sourceforge.net/documentation.html + * @package File_Ogg + * @version CVS: $Id: Flac.php,v 1.9 2005/11/16 20:43:27 djg Exp $ + */ +class File_Ogg_Flac extends File_Ogg_Media +{ + /** + * @access private + */ + function __construct($streamSerial, $streamData, $filePointer) + { + parent::__construct($streamSerial, $streamData, $filePointer); + $this->_decodeHeader(); + $this->_decodeCommentsHeader(); + $this->_streamLength = $this->_streamInfo['total_samples'] + / $this->_streamInfo['sample_rate']; + } + + /** + * Get a short string describing the type of the stream + * @return string + */ + function getType() { + return 'FLAC'; + } + + /** + * @access private + * @param int $packetType + * @param int $pageOffset + */ + function _decodeHeader() + { + fseek($this->_filePointer, $this->_streamData['pages'][0]['body_offset'], SEEK_SET); + // Check if this is the correct header. + $packet = unpack("Cdata", fread($this->_filePointer, 1)); + if ($packet['data'] != 0x7f) + throw new OggException("Stream Undecodable", OGG_ERROR_UNDECODABLE); + + // The following four characters should be "FLAC". + if (fread($this->_filePointer, 4) != 'FLAC') + throw new OggException("Stream is undecodable due to a malformed header.", OGG_ERROR_UNDECODABLE); + + $version = unpack("Cmajor/Cminor", fread($this->_filePointer, 2)); + $this->_version = "{$version['major']}.{$version['minor']}"; + if ($version['major'] > 1) { + throw new OggException("Cannot decode a version {$version['major']} FLAC stream", OGG_ERROR_UNDECODABLE); + } + $h = File_Ogg::_readBigEndian( $this->_filePointer, + array( + // Ogg-specific + 'num_headers' => 16, + 'flac_native_sig' => 32, + // METADATA_BLOCK_HEADER + 'is_last' => 1, + 'type' => 7, + 'length' => 24, + )); + + // METADATA_BLOCK_STREAMINFO + // The variable names are canonical, and come from the FLAC source (format.h) + $this->_streamInfo = File_Ogg::_readBigEndian( $this->_filePointer, + array( + 'min_blocksize' => 16, + 'max_blocksize' => 16, + 'min_framesize' => 24, + 'max_framesize' => 24, + 'sample_rate' => 20, + 'channels' => 3, + 'bits_per_sample' => 5, + 'total_samples' => 36, + )); + $this->_streamInfo['md5sum'] = bin2hex(fread($this->_filePointer, 16)); + } + + /** + * Get an associative array containing header information about the stream + * @access public + * @return array + */ + function getHeader() + { + return $this->_streamInfo; + } + + function _decodeCommentsHeader() + { + fseek($this->_filePointer, $this->_streamData['pages'][1]['body_offset'], SEEK_SET); + $blockHeader = File_Ogg::_readBigEndian( $this->_filePointer, + array( + 'last_block' => 1, + 'block_type' => 7, + 'length' => 24 + ) + ); + if ($blockHeader['block_type'] != 4) { + throw new OggException("Stream Undecodable", OGG_ERROR_UNDECODABLE); + } + + $this->_decodeBareCommentsHeader(); + } +} +?> diff --git a/extensions/TimedMediaHandler/handlers/OggHandler/File_Ogg/File/Ogg/Media.php b/extensions/TimedMediaHandler/handlers/OggHandler/File_Ogg/File/Ogg/Media.php new file mode 100644 index 00000000..67ddaece --- /dev/null +++ b/extensions/TimedMediaHandler/handlers/OggHandler/File_Ogg/File/Ogg/Media.php @@ -0,0 +1,262 @@ +<?php +/* vim: set expandtab tabstop=4 shiftwidth=4: */ +// +----------------------------------------------------------------------------+ +// | File_Ogg PEAR Package for Accessing Ogg Bitstreams | +// | Copyright (c) 2005-2007 | +// | David Grant <david@grant.org.uk> | +// | Tim Starling <tstarling@wikimedia.org> | +// +----------------------------------------------------------------------------+ +// | This library is free software; you can redistribute it and/or | +// | modify it under the terms of the GNU Lesser General Public | +// | License as published by the Free Software Foundation; either | +// | version 2.1 of the License, or (at your option) any later version. | +// | | +// | This library is distributed in the hope that it will be useful, | +// | but WITHOUT ANY WARRANTY; without even the implied warranty of | +// | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | +// | Lesser General Public License for more details. | +// | | +// | You should have received a copy of the GNU Lesser General Public | +// | License along with this library; if not, write to the Free Software | +// | Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA | +// +----------------------------------------------------------------------------+ + + +/** + * Parent class for media bitstreams + * Contains some functions common to various media formats + */ +abstract class File_Ogg_Media extends File_Ogg_Bitstream +{ + /** + * Maximum size of header comment to parse. + * Set to 1 MB by default. Make sure this is less than your PHP memory_limit. + */ + const COMMENT_MAX_SIZE = 1000000; + + /** + * Array to hold each of the comments. + * + * @access private + * @var array + */ + var $_comments = array(); + + /** + * Vendor string for the stream. + * + * @access private + * @var string + */ + var $_vendor; + + /** + * Length of the stream in seconds + */ + var $_streamLength; + + /* Start offset of the stream in seconds */ + var $_startOffset = 0; + + /** + * Get a short string describing the type of the stream + * @return string + */ + abstract function getType(); + + /** + * Get the 6-byte identification string expected in the common header + * @return string + */ + function getIdentificationString() + { + return ''; + } + + /** + * @access private + * @param int $packetType + * @param int $pageOffset + */ + function _decodeCommonHeader($packetType, $pageOffset) + { + fseek($this->_filePointer, $this->_streamData['pages'][$pageOffset]['body_offset'], SEEK_SET); + if ($packetType !== false) { + // Check if this is the correct header. + $packet = unpack("Cdata", fread($this->_filePointer, 1)); + if ($packet['data'] != $packetType) + throw new OggException("Stream Undecodable", OGG_ERROR_UNDECODABLE); + + // The following six characters should be equal to getIdentificationString() + $id = $this->getIdentificationString(); + if ($id !== '' && fread($this->_filePointer, strlen($id)) !== $id) + throw new OggException("Stream is undecodable due to a malformed header.", OGG_ERROR_UNDECODABLE); + } // else seek only, no common header + } + + /** + * Parse a Vorbis-style comments header. + * + * This function parses the comments header. The comments header contains a series of + * UTF-8 comments related to the audio encoded in the stream. This header also contains + * a string to identify the encoding software. More details on the comments header can + * be found at the following location: http://xiph.org/vorbis/doc/v-comment.html + * + * @access private + */ + function _decodeBareCommentsHeader() + { + // Decode the vendor string length as a 32-bit unsigned integer. + $vendor_len = unpack("Vdata", fread($this->_filePointer, 4)); + if ( $vendor_len['data'] > 0 ) { + // Retrieve the vendor string from the stream. + $this->_vendor = fread($this->_filePointer, $vendor_len['data']); + } else { + $this->_vendor = ''; + } + // Decode the size of the comments list as a 32-bit unsigned integer. + $comment_list_length = unpack("Vdata", fread($this->_filePointer, 4)); + // Iterate through the comments list. + for ($i = 0; $i < $comment_list_length['data']; ++$i) { + // Unpack the length of this comment. + $comment_length = unpack("Vdata", fread($this->_filePointer, 4)); + + // If the comment length is greater than specified limit, skip it. + if ( $comment_length['data'] > self::COMMENT_MAX_SIZE ) { + continue; + } + + // Comments are in the format 'ARTIST=Super Furry Animals', so split it on the equals character. + // NOTE: Equals characters are strictly prohibited in either the COMMENT or DATA parts. + $comment = explode("=", fread($this->_filePointer, $comment_length['data'])); + $comment_title = (string) $comment[0]; + $comment_value = (string) $comment[1]; + + // Check if the comment type (e.g. ARTIST) already exists. If it does, + // take the new value, and the existing value (or array) and insert it + // into a new array. This is important, since each comment type may have + // multiple instances (e.g. ARTIST for a collaboration) and we should not + // overwrite the previous value. + if (isset($this->_comments[$comment_title])) { + if (is_array($this->_comments[$comment_title])) + $this->_comments[$comment_title][] = $comment_value; + else + $this->_comments[$comment_title] = array($this->_comments[$comment_title], $comment_value); + } else + $this->_comments[$comment_title] = $comment_value; + } + } + + /** + * Number of channels used in this stream + * + * This function returns the number of channels used in this stream. This + * can range from 1 to 255, but will likely be 2 (stereo) or 1 (mono). + * + * @access public + * @return int + * @see File_Ogg_Vorbis::isMono() + * @see File_Ogg_Vorbis::isStereo() + * @see File_Ogg_Vorbis::isQuadrophonic() + */ + function getChannels() + { + return ($this->_channels); + } + + /** + * Provides a list of the comments extracted from the Vorbis stream. + * + * It is recommended that the user fully inspect the array returned by this function + * rather than blindly requesting a comment in false belief that it will always + * be present. Whilst the Vorbis specification dictates a number of popular + * comments (e.g. TITLE, ARTIST, etc.) for use in Vorbis streams, they are not + * guaranteed to appear. + * + * @access public + * @return array + */ + function getCommentList() + { + return (array_keys($this->_comments)); + } + + /** + * Provides an interface to the numerous comments located with a Vorbis stream. + * + * A Vorbis stream may contain one or more instances of each comment, so the user + * should check the variable type before printing out the result of this method. + * The situation in which multiple instances of a comment occurring are not as + * rare as one might think, since they are conceivable at least for ARTIST comments + * in the situation where a track is a duet. + * + * @access public + * @param string $commentTitle Comment title to search for, e.g. TITLE. + * @param string $separator String to separate multiple values. + * @return string + */ + function getField($commentTitle, $separator = ", ") + { + if (isset($this->_comments[$commentTitle])) { + if (is_array($this->_comments[$commentTitle])) + return (implode($separator, $this->_comments[$commentTitle])); + else + return ($this->_comments[$commentTitle]); + } else + // The comment doesn't exist in this file. The user should've called getCommentList first. + return (""); + } + + /** + * Get the entire comments array. + * May return an empty array if the bitstream does not support comments. + * + * @access public + * @return array + */ + function getComments() { + return $this->_comments; + } + + /** + * Vendor of software used to encode this stream. + * + * Gives the vendor string for the software used to encode this stream. + * It is common to find libVorbis here. The majority of encoders appear + * to use libvorbis from Xiph.org. + * + * @access public + * @return string + */ + function getVendor() + { + return ($this->_vendor); + } + + /** + * Get an associative array containing header information about the stream + * @access public + * @return array + */ + function getHeader() + { + return array(); + } + + /** + * Get the length of the stream in seconds + * @return float + */ + function getLength() + { + return $this->_streamLength; + } + /** + * Get the start offset of the stream in seconds + * + * @return float + */ + function getStartOffset(){ + return $this->_startOffset; + } +} diff --git a/extensions/TimedMediaHandler/handlers/OggHandler/File_Ogg/File/Ogg/Opus.php b/extensions/TimedMediaHandler/handlers/OggHandler/File_Ogg/File/Ogg/Opus.php new file mode 100644 index 00000000..2c23d928 --- /dev/null +++ b/extensions/TimedMediaHandler/handlers/OggHandler/File_Ogg/File/Ogg/Opus.php @@ -0,0 +1,126 @@ +<?php +/* vim: set expandtab tabstop=4 shiftwidth=4: */ +// +----------------------------------------------------------------------------+ +// | File_Ogg PEAR Package for Accessing Ogg Bitstreams | +// | Copyright (c) 2013 | +// | Jan Gerber <jgerber@wikimedia.org> | +// +----------------------------------------------------------------------------+ +// | This library is free software; you can redistribute it and/or | +// | modify it under the terms of the GNU Lesser General Public | +// | License as published by the Free Software Foundation; either | +// | version 2.1 of the License, or (at your option) any later version. | +// | | +// | This library is distributed in the hope that it will be useful, | +// | but WITHOUT ANY WARRANTY; without even the implied warranty of | +// | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | +// | Lesser General Public License for more details. | +// | | +// | You should have received a copy of the GNU Lesser General Public | +// | License along with this library; if not, write to the Free Software | +// | Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA | +// +----------------------------------------------------------------------------+ + + +define( 'OGG_OPUS_COMMENTS_PAGE_OFFSET', 1 ); + +/** + * @author Jan Gerber <jgerber@wikimedia.org> + * @category File + * @copyright Jan Gerber <jgerber@wikimedia.org> + * @license http://www.gnu.org/copyleft/lesser.html GNU LGPL + * @link http://pear.php.net/package/File_Ogg + * @link http://www.opus-codec.org/ + * @package File_Ogg + * @version 1 + */ +class File_Ogg_Opus extends File_Ogg_Media +{ + /** + * @access private + */ + function __construct($streamSerial, $streamData, $filePointer) + { + parent::__construct($streamSerial, $streamData, $filePointer); + $this->_decodeHeader(); + $this->_decodeCommentsHeader(); + + $endSec = $this->getSecondsFromGranulePos( $this->_lastGranulePos ); + $startSec = $this->getSecondsFromGranulePos( $this->_firstGranulePos ); + + if( $startSec > 1){ + $this->_streamLength = $endSec - $startSec; + $this->_startOffset = $startSec; + }else{ + $this->_streamLength = $endSec; + } + $this->_avgBitrate = $this->_streamLength ? ($this->_streamSize * 8) / $this->_streamLength : 0; + } + + function getSecondsFromGranulePos( $granulePos ){ + return (( '0x' . substr( $granulePos, 0, 8 ) ) * pow(2, 32) + + ( '0x' . substr( $granulePos, 8, 8 ) ) + - $this->_header['pre_skip']) + / 48000; + } + + /** + * Get a short string describing the type of the stream + * @return string + */ + function getType() + { + return 'Opus'; + } + + /** + * Decode the stream header + * @access private + */ + function _decodeHeader() + { + fseek($this->_filePointer, $this->_streamData['pages'][0]['body_offset'], SEEK_SET); + // The first 8 characters should be "OpusHead". + if (fread($this->_filePointer, 8) != 'OpusHead') + throw new OggException("Stream is undecodable due to a malformed header.", OGG_ERROR_UNDECODABLE); + + $this->_header = File_Ogg::_readLittleEndian($this->_filePointer, array( + 'opus_version' => 8, + 'nb_channels' => 8, + 'pre_skip' => 16, + 'audio_sample_rate' => 32, + 'output_gain' => 16, + 'channel_mapping_family'=> 8, + )); + $this->_channels = $this->_header['nb_channels']; + } + + /** + * Get an associative array containing header information about the stream + * @access public + * @return array + */ + function getHeader() { + return $this->_header; + } + + function getSampleRate() + { + //Opus always outputs 48kHz, the header only lists + //the samplerate of the source as reference + return 48000; + } + + /** + * Decode the comments header + * @access private + */ + function _decodeCommentsHeader() + { + $id = 'OpusTags'; + $this->_decodeCommonHeader(false, OGG_OPUS_COMMENTS_PAGE_OFFSET); + if(fread($this->_filePointer, strlen($id)) !== $id) + throw new OggException("Stream is undecodable due to a malformed header.", OGG_ERROR_UNDECODABLE); + $this->_decodeBareCommentsHeader(); + } +} +?> diff --git a/extensions/TimedMediaHandler/handlers/OggHandler/File_Ogg/File/Ogg/Speex.php b/extensions/TimedMediaHandler/handlers/OggHandler/File_Ogg/File/Ogg/Speex.php new file mode 100644 index 00000000..42f9b0eb --- /dev/null +++ b/extensions/TimedMediaHandler/handlers/OggHandler/File_Ogg/File/Ogg/Speex.php @@ -0,0 +1,122 @@ +<?php +/* vim: set expandtab tabstop=4 shiftwidth=4: */ +// +----------------------------------------------------------------------------+ +// | File_Ogg PEAR Package for Accessing Ogg Bitstreams | +// | Copyright (c) 2005-2007 | +// | David Grant <david@grant.org.uk> | +// | Tim Starling <tstarling@wikimedia.org> | +// +----------------------------------------------------------------------------+ +// | This library is free software; you can redistribute it and/or | +// | modify it under the terms of the GNU Lesser General Public | +// | License as published by the Free Software Foundation; either | +// | version 2.1 of the License, or (at your option) any later version. | +// | | +// | This library is distributed in the hope that it will be useful, | +// | but WITHOUT ANY WARRANTY; without even the implied warranty of | +// | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | +// | Lesser General Public License for more details. | +// | | +// | You should have received a copy of the GNU Lesser General Public | +// | License along with this library; if not, write to the Free Software | +// | Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA | +// +----------------------------------------------------------------------------+ + + +/** + * @author David Grant <david@grant.org.uk>, Tim Starling <tstarling@wikimedia.org> + * @category File + * @copyright David Grant <david@grant.org.uk>, Tim Starling <tstarling@wikimedia.org> + * @license http://www.gnu.org/copyleft/lesser.html GNU LGPL + * @link http://pear.php.net/package/File_Ogg + * @link http://www.speex.org/docs.html + * @package File_Ogg + * @version CVS: $Id: Speex.php,v 1.10 2005/11/16 20:43:27 djg Exp $ + */ +class File_Ogg_Speex extends File_Ogg_Media +{ + /** + * @access private + */ + function __construct($streamSerial, $streamData, $filePointer) + { + parent::__construct($streamSerial, $streamData, $filePointer); + $this->_decodeHeader(); + $this->_decodeCommentsHeader(); + $endSec = + (( '0x' . substr( $this->_lastGranulePos, 0, 8 ) ) * pow(2, 32) + + ( '0x' . substr( $this->_lastGranulePos, 8, 8 ) )) + / $this->_header['rate']; + + $startSec = + (( '0x' . substr( $this->_firstGranulePos, 0, 8 ) ) * pow(2, 32) + + ( '0x' . substr( $this->_firstGranulePos, 8, 8 ) )) + / $this->_header['rate']; + + //make sure the offset is worth taking into account oggz_chop related hack + if( $startSec > 1){ + $this->_streamLength = $endSec - $startSec; + $this->_startOffset = $startSec; + }else{ + $this->_streamLength = $endSec; + } + } + + /** + * Get a short string describing the type of the stream + * @return string + */ + function getType() + { + return 'Speex'; + } + + /** + * Decode the stream header + * @access private + */ + function _decodeHeader() + { + fseek($this->_filePointer, $this->_streamData['pages'][0]['body_offset'], SEEK_SET); + // The first 8 characters should be "Speex ". + if (fread($this->_filePointer, 8) != 'Speex ') + throw new OggException("Stream is undecodable due to a malformed header.", OGG_ERROR_UNDECODABLE); + + $this->_version = fread($this->_filePointer, 20); + $this->_header = File_Ogg::_readLittleEndian($this->_filePointer, array( + 'speex_version_id' => 32, + 'header_size' => 32, + 'rate' => 32, + 'mode' => 32, + 'mode_bitstream_version'=> 32, + 'nb_channels' => 32, + 'bitrate' => 32, + 'frame_size' => 32, + 'vbr' => 32, + 'frames_per_packet' => 32, + 'extra_headers' => 32, + 'reserved1' => 32, + 'reserved2' => 32 + )); + $this->_header['speex_version'] = $this->_version; + } + + /** + * Get an associative array containing header information about the stream + * @access public + * @return array + */ + function getHeader() { + return $this->_header; + } + + /** + * Decode the comments header + * @access private + */ + function _decodeCommentsHeader() + { + fseek($this->_filePointer, $this->_streamData['pages'][1]['body_offset'], SEEK_SET); + $this->_decodeBareCommentsHeader(); + } +} +?> diff --git a/extensions/TimedMediaHandler/handlers/OggHandler/File_Ogg/File/Ogg/Theora.php b/extensions/TimedMediaHandler/handlers/OggHandler/File_Ogg/File/Ogg/Theora.php new file mode 100644 index 00000000..b801ea26 --- /dev/null +++ b/extensions/TimedMediaHandler/handlers/OggHandler/File_Ogg/File/Ogg/Theora.php @@ -0,0 +1,240 @@ +<?php +/* vim: set expandtab tabstop=4 shiftwidth=4: */ +// +----------------------------------------------------------------------------+ +// | File_Ogg PEAR Package for Accessing Ogg Bitstreams | +// | Copyright (c) 2005-2007 | +// | David Grant <david@grant.org.uk> | +// | Tim Starling <tstarling@wikimedia.org> | +// +----------------------------------------------------------------------------+ +// | This library is free software; you can redistribute it and/or | +// | modify it under the terms of the GNU Lesser General Public | +// | License as published by the Free Software Foundation; either | +// | version 2.1 of the License, or (at your option) any later version. | +// | | +// | This library is distributed in the hope that it will be useful, | +// | but WITHOUT ANY WARRANTY; without even the implied warranty of | +// | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | +// | Lesser General Public License for more details. | +// | | +// | You should have received a copy of the GNU Lesser General Public | +// | License along with this library; if not, write to the Free Software | +// | Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA | +// +----------------------------------------------------------------------------+ + + +define( 'OGG_THEORA_IDENTIFICATION_HEADER', 0x80 ); +define( 'OGG_THEORA_COMMENTS_HEADER', 0x81 ); +define( 'OGG_THEORA_IDENTIFICATION_PAGE_OFFSET', 0 ); +define( 'OGG_THEORA_COMMENTS_PAGE_OFFSET', 1 ); + + +/** + * @author David Grant <david@grant.org.uk>, Tim Starling <tstarling@wikimedia.org> + * @category File + * @copyright David Grant <david@grant.org.uk>, Tim Starling <tstarling@wikimedia.org> + * @license http://www.gnu.org/copyleft/lesser.html GNU LGPL + * @link http://pear.php.net/package/File_Ogg + * @link http://www.xiph.org/theora/ + * @package File_Ogg + * @version CVS: $Id: Theora.php,v 1.9 2005/11/16 20:43:27 djg Exp $ + */ +class File_Ogg_Theora extends File_Ogg_Media +{ + /** + * @access private + */ + function __construct($streamSerial, $streamData, $filePointer) + { + parent::__construct($streamSerial, $streamData, $filePointer); + $this->_decodeIdentificationHeader(); + $this->_decodeCommentsHeader(); + $endSec = $this->getSecondsFromGranulePos( $this->_lastGranulePos ); + + $startSec = $this->getSecondsFromGranulePos( $this->_firstGranulePos ); + + //make sure the offset is worth taking into account oggz_chop related hack + if( $startSec > 1){ + $this->_streamLength = $endSec - $startSec; + $this->_startOffset = $startSec; + }else{ + $this->_streamLength = $endSec; + } + + $this->_avgBitrate = $this->_streamLength ? ($this->_streamSize * 8) / $this->_streamLength : 0; + } + function getSecondsFromGranulePos($granulePos){ + // Calculate GranulePos seconds + // First make some "numeric strings" + // These might not fit into PHP's integer type, but they will fit into + // the 53-bit mantissa of a double-precision number + $topWord = floatval( base_convert( substr( $granulePos, 0, 8 ), 16, 10 ) ); + $bottomWord = floatval( base_convert( substr( $granulePos, 8, 8 ), 16, 10 ) ); + // Calculate the keyframe position by shifting right by KFGSHIFT + // We don't use PHP's shift operators because they're terribly broken + // This is made slightly simpler by the fact that KFGSHIFT < 32 + $keyFramePos = $topWord / pow(2, $this->_kfgShift - 32) + + floor( $bottomWord / pow(2, $this->_kfgShift) ); + // Calculate the frame offset by masking off the top 64-KFGSHIFT bits + // This requires a bit of floating point trickery + $offset = fmod( $bottomWord, pow(2, $this->_kfgShift) ); + // They didn't teach you that one at school did they? + // Now put it together with the frame rate to calculate time in seconds + return ( $keyFramePos + $offset ) / $this->_frameRate; + } + /** + * Get the 6-byte identification string expected in the common header + */ + function getIdentificationString() + { + return OGG_STREAM_CAPTURE_THEORA; + } + + /** + * Parse the identification header in a Theora stream. + * @access private + */ + function _decodeIdentificationHeader() + { + $this->_decodeCommonHeader(OGG_THEORA_IDENTIFICATION_HEADER, OGG_THEORA_IDENTIFICATION_PAGE_OFFSET); + $h = File_Ogg::_readBigEndian( $this->_filePointer, array( + 'VMAJ' => 8, + 'VMIN' => 8, + 'VREV' => 8, + 'FMBW' => 16, + 'FMBH' => 16, + 'PICW' => 24, + 'PICH' => 24, + 'PICX' => 8, + 'PICY' => 8, + 'FRN' => 32, + 'FRD' => 32, + 'PARN' => 24, + 'PARD' => 24, + 'CS' => 8, + 'NOMBR' => 24, + 'QUAL' => 6, + 'KFGSHIFT' => 5, + 'PF' => 2)); + if ( !$h ) { + throw new OggException("Stream is undecodable due to a truncated header.", OGG_ERROR_UNDECODABLE); + } + + // Theora version + // Seems overly strict but this is what the spec says + // VREV is for backwards-compatible changes, apparently + if ( $h['VMAJ'] != 3 || $h['VMIN'] != 2 ) { + throw new OggException("Stream is undecodable due to an invalid theora version.", OGG_ERROR_UNDECODABLE); + } + $this->_theoraVersion = "{$h['VMAJ']}.{$h['VMIN']}.{$h['VREV']}"; + + // Frame height/width + if ( !$h['FMBW'] || !$h['FMBH'] ) { + throw new OggException("Stream is undecodable because it has frame size of zero.", OGG_ERROR_UNDECODABLE); + } + $this->_frameWidth = $h['FMBW'] * 16; + $this->_frameHeight = $h['FMBH'] * 16; + + // Picture height/width + if ( $h['PICW'] > $this->_frameWidth || $h['PICH'] > $this->_frameHeight ) { + throw new OggException("Stream is undecodable because the picture width is greater than the frame width.", OGG_ERROR_UNDECODABLE); + } + $this->_pictureWidth = $h['PICW']; + $this->_pictureHeight = $h['PICH']; + + // Picture offset + $this->_offsetX = $h['PICX']; + $this->_offsetY = $h['PICY']; + // Frame rate + $this->_frameRate = $h['FRD'] == 0 ? 0 : $h['FRN'] / $h['FRD']; + // Physical aspect ratio + if ( !$h['PARN'] || !$h['PARD'] ) { + $this->_physicalAspectRatio = 1; + } else { + $this->_physicalAspectRatio = $h['PARN'] / $h['PARD']; + } + + // Color space + $colorSpaces = array( + 0 => 'Undefined', + 1 => 'Rec. 470M', + 2 => 'Rec. 470BG', + ); + if ( isset( $colorSpaces[$h['CS']] ) ) { + $this->_colorSpace = $colorSpaces[$h['CS']]; + } else { + $this->_colorSpace = 'Unknown (reserved)'; + } + + $this->_nomBitrate = $h['NOMBR']; + + $this->_quality = $h['QUAL']; + $this->_kfgShift = $h['KFGSHIFT']; + + $pixelFormats = array( + 0 => '4:2:0', + 1 => 'Unknown (reserved)', + 2 => '4:2:2', + 3 => '4:4:4', + ); + $this->_pixelFormat = $pixelFormats[$h['PF']]; + + switch ( $h['PF'] ) { + case 0: + $h['NSBS'] = + floor( ($h['FMBW'] + 1) / 2 ) * + floor( ($h['FMBH'] + 1) / 2 ) + 2 * + floor( ($h['FMBW'] + 3) / 4 ) * + floor( ($h['FMBH'] + 3) / 4 ); + $h['NBS'] = 6 * $h['FMBW'] * $h['FMBH']; + break; + case 2: + $h['NSBS'] = + floor( ($h['FMBW'] + 1) / 2 ) * + floor( ($h['FMBH'] + 1) / 2 ) + 2 * + floor( ($h['FMBW'] + 3) / 4 ) * + floor( ($h['FMBH'] + 1) / 2 ); + $h['NBS'] = 8 * $h['FMBW'] * $h['FMBH']; + break; + case 3: + $h['NSBS'] = + 3 * floor( ($h['FMBW'] + 1) / 2 ) * + floor( ($h['FMBH'] + 1) / 2 ); + $h['NBS'] = 12 * $h['FMBW'] * $h['FMBH']; + break; + default: + $h['NSBS'] = $h['NBS'] = 0; + } + $h['NMBS'] = $h['FMBW'] * $h['FMBH']; + + $this->_idHeader = $h; + } + + /** + * Get an associative array containing header information about the stream + * @access public + * @return array + */ + function getHeader() { + return $this->_idHeader; + } + + /** + * Get a short string describing the type of the stream + * @return string + */ + function getType() { + return 'Theora'; + } + + /** + * Decode the comments header + * @access private + */ + function _decodeCommentsHeader() + { + $this->_decodeCommonHeader(OGG_THEORA_COMMENTS_HEADER, OGG_THEORA_COMMENTS_PAGE_OFFSET); + $this->_decodeBareCommentsHeader(); + } + +} +?> diff --git a/extensions/TimedMediaHandler/handlers/OggHandler/File_Ogg/File/Ogg/Vorbis.php b/extensions/TimedMediaHandler/handlers/OggHandler/File_Ogg/File/Ogg/Vorbis.php new file mode 100644 index 00000000..06c2c180 --- /dev/null +++ b/extensions/TimedMediaHandler/handlers/OggHandler/File_Ogg/File/Ogg/Vorbis.php @@ -0,0 +1,790 @@ +<?php +/* vim: set expandtab tabstop=4 shiftwidth=4: */ +// +----------------------------------------------------------------------------+ +// | File_Ogg PEAR Package for Accessing Ogg Bitstreams | +// | Copyright (c) 2005-2007 | +// | David Grant <david@grant.org.uk> | +// | Tim Starling <tstarling@wikimedia.org> | +// +----------------------------------------------------------------------------+ +// | This library is free software; you can redistribute it and/or | +// | modify it under the terms of the GNU Lesser General Public | +// | License as published by the Free Software Foundation; either | +// | version 2.1 of the License, or (at your option) any later version. | +// | | +// | This library is distributed in the hope that it will be useful, | +// | but WITHOUT ANY WARRANTY; without even the implied warranty of | +// | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | +// | Lesser General Public License for more details. | +// | | +// | You should have received a copy of the GNU Lesser General Public | +// | License along with this library; if not, write to the Free Software | +// | Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA | +// +----------------------------------------------------------------------------+ + + +/** + * Check number for the first header in a Vorbis stream. + * + * @access private + */ +define("OGG_VORBIS_IDENTIFICATION_HEADER", 1); +/** + * Check number for the second header in a Vorbis stream. + * + * @access private + */ +define("OGG_VORBIS_COMMENTS_HEADER", 3); +/** + * Check number for the third header in a Vorbis stream. + * + * @access private + */ +define("OGG_VORBIS_SETUP_HEADER", 5); +/** + * Error thrown if the stream appears to be corrupted. + * + * @access private + */ +define("OGG_VORBIS_ERROR_UNDECODABLE", OGG_ERROR_UNDECODABLE); +/** + * Error thrown if the user attempts to extract a comment using a comment key + * that does not exist. + * + * @access private + */ +define("OGG_VORBIS_ERROR_INVALID_COMMENT", 2); + +define("OGG_VORBIS_IDENTIFICATION_PAGE_OFFSET", 0); +define("OGG_VORBIS_COMMENTS_PAGE_OFFSET", 1); + +/** + * Error thrown if the user attempts to write a comment containing an illegal + * character + * + * @access private + */ +define("OGG_VORBIS_ERROR_ILLEGAL_COMMENT", 3); + +/** + * Extract the contents of a Vorbis logical stream. + * + * This class provides an interface to a Vorbis logical stream found within + * a Ogg stream. A variety of information may be extracted, including comment + * tags, running time, and bitrate. For more information, please see the following + * links. + * + * @author David Grant <david@grant.org.uk>, Tim Starling <tstarling@wikimedia.org> + * @category File + * @copyright David Grant <david@grant.org.uk>, Tim Starling <tstarling@wikimedia.org> + * @license http://www.gnu.org/copyleft/lesser.html GNU LGPL + * @link http://pear.php.net/package/File_Ogg + * @link http://www.xiph.org/vorbis/doc/ + * @package File_Ogg + * @version CVS: $Id: Vorbis.php,v 1.13 2005/11/19 09:06:32 djg Exp $ + */ +class File_Ogg_Vorbis extends File_Ogg_Media +{ + + /** + * Version of vorbis specification used. + * + * @access private + * @var int + */ + var $_version; + + /** + * Number of channels in the vorbis stream. + * + * @access private + * @var int + */ + var $_channels; + + /** + * Number of samples per second in the vorbis stream. + * + * @access private + * @var int + */ + var $_sampleRate; + + /** + * Minimum bitrate for the vorbis stream. + * + * @access private + * @var int + */ + var $_minBitrate; + + /** + * Maximum bitrate for the vorbis stream. + * + * @access private + * @var int + */ + var $_maxBitrate; + + /** + * Nominal bitrate for the vorbis stream. + * + * @access private + * @var int + */ + var $_nomBitrate; + + /** + * Average bitrate for the vorbis stream. + * + * @access private + * @var float + */ + var $_avgBitrate; + + /** + * The length of this stream in seconds. + * + * @access private + * @var int + */ + var $_streamLength; + + /** + * the start offset of this stream in seconds + */ + var $_startOffset; + /** + * Constructor for accessing a Vorbis logical stream. + * + * This method is the constructor for the native-PHP interface to a Vorbis logical + * stream, embedded within an Ogg physical stream. + * + * @param int $streamSerial Serial number of the logical stream. + * @param array $streamData Data for the requested logical stream. + * @param string $filePath Location of a file on the filesystem. + * @param pointer $filePointer File pointer for the current physical stream. + * @access private + */ + function __construct($streamSerial, $streamData, $filePointer) + { + parent::__construct($streamSerial, $streamData, $filePointer); + $this->_decodeIdentificationHeader(); + $this->_decodeCommentsHeader(OGG_VORBIS_COMMENTS_HEADER, OGG_VORBIS_COMMENTS_PAGE_OFFSET); + + $endSec = $this->getSecondsFromGranulePos( $this->_lastGranulePos ); + $startSec = $this->getSecondsFromGranulePos( $this->_firstGranulePos ); + + //make sure the offset is worth taking into account oggz_chop related hack + if( $startSec > 1){ + $this->_streamLength = $endSec - $startSec; + $this->_startOffset = $startSec; + }else{ + $this->_streamLength = $endSec; + } + + $this->_avgBitrate = $this->_streamLength ? ($this->_streamSize * 8) / $this->_streamLength : 0; + } + function getSecondsFromGranulePos( $granulePos ){ + return (( '0x' . substr( $granulePos, 0, 8 ) ) * pow(2, 32) + + ( '0x' . substr( $granulePos, 8, 8 ) )) + / $this->_idHeader['audio_sample_rate']; + } + /** + * Get a short string describing the type of the stream + */ + function getType() + { + return 'Vorbis'; + } + + /** + * Parse the identification header (the first of three headers) in a Vorbis stream. + * + * This function parses the identification header. The identification header + * contains simple audio characteristics, such as sample rate and number of + * channels. There are a number of error-checking provisions laid down in the Vorbis + * specification to ensure the stream is pure. + * + * @access private + */ + function _decodeIdentificationHeader() + { + $this->_decodeCommonHeader(OGG_VORBIS_IDENTIFICATION_HEADER, OGG_VORBIS_IDENTIFICATION_PAGE_OFFSET); + + $h = File_Ogg::_readLittleEndian($this->_filePointer, array( + 'vorbis_version' => 32, + 'audio_channels' => 8, + 'audio_sample_rate' => 32, + 'bitrate_maximum' => 32, + 'bitrate_nominal' => 32, + 'bitrate_minimum' => 32, + 'blocksize_0' => 4, + 'blocksize_1' => 4, + 'framing_flag' => 1 + )); + + // The Vorbis stream version must be 0. + if ($h['vorbis_version'] == 0) + $this->_version = $h['vorbis_version']; + else + throw new OggException("Stream is undecodable due to an invalid vorbis stream version.", OGG_VORBIS_ERROR_UNDECODABLE); + + // The number of channels MUST be greater than 0. + if ($h['audio_channels'] == 0) + throw new OggException("Stream is undecodable due to zero channels.", OGG_VORBIS_ERROR_UNDECODABLE); + else + $this->_channels = $h['audio_channels']; + + // The sample rate MUST be greater than 0. + if ($h['audio_sample_rate'] == 0) + throw new OggException("Stream is undecodable due to a zero sample rate.", OGG_VORBIS_ERROR_UNDECODABLE); + else + $this->_sampleRate = $h['audio_sample_rate']; + + // Extract the various bitrates + $this->_maxBitrate = $h['bitrate_maximum']; + $this->_nomBitrate = $h['bitrate_nominal']; + $this->_minBitrate = $h['bitrate_minimum']; + + // Powers of two between 6 and 13 inclusive. + $valid_block_sizes = array(64, 128, 256, 512, 1024, 2048, 4096, 8192); + + // blocksize_0 MUST be a valid blocksize. + $blocksize_0 = pow(2, $h['blocksize_0']); + if (FALSE == in_array($blocksize_0, $valid_block_sizes)) + throw new OggException("Stream is undecodable because blocksize_0 is $blocksize_0, which is not a valid size.", OGG_VORBIS_ERROR_UNDECODABLE); + + // Extract bits 5 to 8 from the character data. + // blocksize_1 MUST be a valid blocksize. + $blocksize_1 = pow(2, $h['blocksize_1']); + if (FALSE == in_array($blocksize_1, $valid_block_sizes)) + throw new OggException("Stream is undecodable because blocksize_1 is not a valid size.", OGG_VORBIS_ERROR_UNDECODABLE); + + // blocksize 0 MUST be less than or equal to blocksize 1. + if ($blocksize_0 > $blocksize_1) + throw new OggException("Stream is undecodable because blocksize_0 is not less than or equal to blocksize_1.", OGG_VORBIS_ERROR_UNDECODABLE); + + // The framing bit MUST be set to mark the end of the identification header. + // Some encoders are broken though -- TS + /* + if ($h['framing_flag'] == 0) + throw new OggException("Stream in undecodable because the framing bit is not non-zero.", OGG_VORBIS_ERROR_UNDECODABLE); + */ + + $this->_idHeader = $h; + } + + /** + * Decode the comments header + * @access private + * @param int $packetType + * @param int $pageOffset + */ + function _decodeCommentsHeader($packetType, $pageOffset) + { + $this->_decodeCommonHeader($packetType, $pageOffset); + $this->_decodeBareCommentsHeader(); + // The framing bit MUST be set to mark the end of the comments header. + $framing_bit = unpack("Cdata", fread($this->_filePointer, 1)); + if ($framing_bit['data'] != 1) + throw new OggException("Stream Undecodable", OGG_VORBIS_ERROR_UNDECODABLE); + } + + /** + * Get the 6-byte identification string expected in the common header + */ + function getIdentificationString() { + return OGG_STREAM_CAPTURE_VORBIS; + } + + /** + * Version of the Vorbis specification referred to in the encoding of this stream. + * + * This method returns the version of the Vorbis specification (currently 0 (ZERO)) + * referred to by the encoder of this stream. The Vorbis specification is well- + * defined, and thus one does not expect this value to change on a frequent basis. + * + * @access public + * @return int + */ + function getEncoderVersion() + { + return ($this->_version); + } + + /** + * Samples per second. + * + * This function returns the number of samples used per second in this + * recording. Probably the most common value here is 44,100. + * + * @return int + * @access public + */ + function getSampleRate() + { + return ($this->_sampleRate); + } + + /** + * Various bitrate measurements + * + * Gives an array of the values of four different types of bitrates for this + * stream. The nominal, maximum and minimum values are found within the file, + * whereas the average value is computed. + * + * @access public + * @return array + */ + function getBitrates() + { + return (array("nom" => $this->_nomBitrate, "max" => $this->_maxBitrate, "min" => $this->_minBitrate, "avg" => $this->_avgBitrate)); + } + + /** + * Gives the most accurate bitrate measurement from this stream. + * + * This function returns the most accurate bitrate measurement for this + * recording, depending on values set in the stream header. + * + * @access public + * @return float + */ + function getBitrate() + { + if ($this->_avgBitrate != 0) + return ($this->_avgBitrate); + elseif ($this->_nomBitrate != 0) + return ($this->_nomBitrate); + else + return (($this->_minBitrate + $this->_maxBitrate) / 2); + } + + /** + * Gives the length (in seconds) of this stream. + * + * @access public + * @return int + */ + function getLength() + { + return ($this->_streamLength); + } + /** + * Get the start offset of the stream in seconds + * @access public + * @return int + */ + function getStartOffset(){ + return ($this->_startOffset); + } + /** + * States whether this logical stream was encoded in mono. + * + * @access public + * @return boolean + */ + function isMono() + { + return ($this->_channels == 1); + } + + /** + * States whether this logical stream was encoded in stereo. + * + * @access public + * @return boolean + */ + function isStereo() + { + return ($this->_channels == 2); + } + + /** + * States whether this logical stream was encoded in quadrophonic sound. + * + * @access public + * @return boolean + */ + function isQuadrophonic() + { + return ($this->_channels == 4); + } + + /** + * The title of this track, e.g. "What's Up Pussycat?". + * + * @access public + * @return string + */ + function getTitle() + { + return ($this->getField("TITLE")); + } + + /** + * Set the title of this track. + * + * @access public + * @param string $title + * @param boolean $replace + */ + function setTitle($title, $replace = true) + { + $this->setField("TITLE", $title, $replace); + } + + /** + * The version of the track, such as a remix. + * + * @access public + * @return string + */ + function getVersion() + { + return $this->getField("VERSION"); + } + + /** + * Set the version of this track. + * + * @access public + * @param string $version + * @param boolean $replace + */ + function setVersion($version, $replace = true) + { + $this->setField("VERSION", $version, $replace); + } + + /** + * The album or collection from which this track comes. + * + * @access public + * @return string + */ + function getAlbum() + { + return ($this->getField("ALBUM")); + } + + /** + * Set the album or collection for this track. + * + * @access public + * @param string $album + * @param boolean $replace + */ + function setAlbum($album, $replace = true) + { + $this->setField("ALBUM", $album, $replace); + } + + /** + * The number of this track if it is part of a larger collection. + * + * @access public + * @return string + */ + function getTrackNumber() + { + return ($this->getField("TRACKNUMBER")); + } + + /** + * Set the number of this relative to the collection. + * + * @access public + * @param int $number + * @param boolean $replace + */ + function setTrackNumber($number, $replace = true) + { + $this->setField("TRACKNUMBER", $number, $replace); + } + + /** + * The artist responsible for this track. + * + * This function returns the name of the artist responsible for this + * recording, which may be either a solo-artist, duet or group. + * + * @access public + * @return string + */ + function getArtist() + { + return ($this->getField("ARTIST")); + } + + /** + * Set the artist of this track. + * + * @access public + * @param string $artist + * @param boolean $replace + */ + function setArtist($artist, $replace = true) + { + $this->setField("ARTIST", $artist, $replace = true); + } + + /** + * The performer of this track, such as an orchestra + * + * @access public + * @return string + */ + function getPerformer() + { + return ($this->getField("PERFORMER")); + } + + /** + * Set the performer of this track. + * + * @access public + * @param string $performer + * @param boolean $replace + */ + function setPerformer($performer, $replace = true) + { + $this->setField("PERFORMER", $performer, $replace); + } + + /** + * The copyright attribution for this track. + * + * @access public + * @return string + */ + function getCopyright() + { + return ($this->getField("COPYRIGHT")); + } + + /** + * Set the copyright attribution for this track. + * + * @access public + * @param string $copyright + * @param boolean $replace + */ + function setCopyright($copyright, $replace = true) + { + $this->setField("COPYRIGHT", $copyright, $replace); + } + + /** + * The rights of distribution for this track. + * + * This funtion returns the license for this track, and may include + * copyright information, or a creative commons statement. + * + * @access public + * @return string + */ + function getLicense() + { + return ($this->getField("LICENSE")); + } + + /** + * Set the distribution rights for this track. + * + * @access public + * @param string $license + * @param boolean $replace + */ + function setLicense($license, $replace = true) + { + $this->setField("LICENSE", $license, $replace); + } + + /** + * The organisation responsible for this track. + * + * This function returns the name of the organisation responsible for + * the production of this track, such as the record label. + * + * @access public + * @return string + */ + function getOrganization() + { + return ($this->getField("ORGANIZATION")); + } + + /** + * Set the organisation responsible for this track. + * + * @access public + * @param string $organization + * @param boolean $replace + */ + function setOrganziation($organization, $replace = true) + { + $this->setField("ORGANIZATION", $organization, $replace); + } + + /** + * A short description of the contents of this track. + * + * This function returns a short description of this track, which might + * contain extra information that doesn't fit anywhere else. + * + * @access public + * @return string + */ + function getDescription() + { + return ($this->getField("DESCRIPTION")); + } + + /** + * Set the description of this track. + * + * @access public + * @param string $description + * @param boolean $replace + */ + function setDescription($description, $replace = true) + { + $this->setField("DESCRIPTION", $replace); + } + + /** + * The genre of this recording (e.g. Rock) + * + * This function returns the genre of this recording. There are no pre- + * defined genres, so this is completely up to the tagging software. + * + * @access public + * @return string + */ + function getGenre() + { + return ($this->getField("GENRE")); + } + + /** + * Set the genre of this track. + * + * @access public + * @param string $genre + * @param boolean $replace + */ + function setGenre($genre, $replace = true) + { + $this->setField("GENRE", $genre, $replace); + } + + /** + * The date of the recording of this track. + * + * This function returns the date on which this recording was made. There + * is no specification for the format of this date. + * + * @access public + * @return string + */ + function getDate() + { + return ($this->getField("DATE")); + } + + /** + * Set the date of recording for this track. + * + * @access public + * @param string $date + * @param boolean $replace + */ + function setDate($date, $replace = true) + { + $this->setField("DATE", $date, $replace); + } + + /** + * Where this recording was made. + * + * This function returns where this recording was made, such as a recording + * studio, or concert venue. + * + * @access public + * @return string + */ + function getLocation() + { + return ($this->getField("LOCATION")); + } + + /** + * Set the location of the recording of this track. + * + * @access public + * @param string $location + * @param boolean $replace + */ + function setLocation($location, $replace = true) + { + $this->setField("LOCATION", $location, $replace); + } + + /** + * @access public + * @return string + */ + function getContact() + { + return ($this->getField("CONTACT")); + } + + /** + * Set the contact information for this track. + * + * @access public + * @param string $contact + * @param boolean $replace + */ + function setContact($contact, $replace = true) + { + $this->setField("CONTACT", $contact, $replace); + } + + /** + * International Standard Recording Code. + * + * Returns the International Standard Recording Code. This code can be + * validated using the Validate_ISPN package. + * + * @access public + * @return string + */ + function getIsrc() + { + return ($this->getField("ISRC")); + } + + /** + * Set the ISRC for this track. + * + * @access public + * @param string $isrc + * @param boolean $replace + */ + function setIsrc($isrc, $replace = true) + { + $this->setField("ISRC", $isrc, $replace); + } + + /** + * Get an associative array containing header information about the stream + * @access public + * @return array + */ + function getHeader() { + return $this->_idHeader; + } +} +?> diff --git a/extensions/TimedMediaHandler/handlers/OggHandler/File_Ogg/File/README b/extensions/TimedMediaHandler/handlers/OggHandler/File_Ogg/File/README new file mode 100644 index 00000000..963ca07c --- /dev/null +++ b/extensions/TimedMediaHandler/handlers/OggHandler/File_Ogg/File/README @@ -0,0 +1,2 @@ +This was originally a fork of the File_Ogg package in PEAR. + diff --git a/extensions/TimedMediaHandler/handlers/OggHandler/OggException.php b/extensions/TimedMediaHandler/handlers/OggHandler/OggException.php new file mode 100644 index 00000000..a09d36da --- /dev/null +++ b/extensions/TimedMediaHandler/handlers/OggHandler/OggException.php @@ -0,0 +1,4 @@ +<?php + +class OggException extends Exception { +} diff --git a/extensions/TimedMediaHandler/handlers/OggHandler/OggHandler.php b/extensions/TimedMediaHandler/handlers/OggHandler/OggHandler.php new file mode 100644 index 00000000..83e1a0e7 --- /dev/null +++ b/extensions/TimedMediaHandler/handlers/OggHandler/OggHandler.php @@ -0,0 +1,379 @@ +<?php +/** + * ogg handler + */ +class OggHandlerTMH extends TimedMediaHandler { + const METADATA_VERSION = 2; + + /** + * @param $image File + * @param $path string + * @return string + */ + function getMetadata( $image, $path ) { + $metadata = array( 'version' => self::METADATA_VERSION ); + + try { + $f = new File_Ogg( $path ); + $streams = array(); + foreach ( $f->listStreams() as $streamIDs ) { + foreach ( $streamIDs as $streamID ) { + $stream = $f->getStream( $streamID ); + $streams[$streamID] = array( + 'serial' => $stream->getSerial(), + 'group' => $stream->getGroup(), + 'type' => $stream->getType(), + 'vendor' => $stream->getVendor(), + 'length' => $stream->getLength(), + 'size' => $stream->getSize(), + 'header' => $stream->getHeader(), + 'comments' => $stream->getComments() + ); + } + } + $metadata['streams'] = $streams; + $metadata['length'] = $f->getLength(); + // Get the offset of the file (in cases where the file is a segment copy) + $metadata['offset'] = $f->getStartOffset(); + } catch ( OggException $e ) { + // File not found, invalid stream, etc. + $metadata['error'] = array( + 'message' => $e->getMessage(), + 'code' => $e->getCode() + ); + } + return serialize( $metadata ); + } + + /** + * Display metadata box on file description page. + * + * This is pretty basic, it puts data from all the streams together, + * and only outputs a couple of the most commonly used ogg "comments", + * with comments from all the streams combined + * + * @param File $file + * @param bool|IContextSource $context Context to use (optional) + * @return array|bool + */ + public function formatMetadata( $file, $context = false ) { + $meta = $this->getCommonMetaArray( $file ); + if ( count( $meta ) === 0 ) { + return false; + } + return $this->formatMetadataHelper( $meta, $context ); + } + + /** + * Get some basic metadata properties that are common across file types. + * + * @param File $file + * @return array Array of metadata. See MW's FormatMetadata class for format. + */ + public function getCommonMetaArray( File $file ) { + $metadata = $this->unpackMetadata( $file->getMetadata() ); + if ( !$metadata || isset( $metadata['error'] ) || !isset( $metadata['streams'] ) ) { + return array(); + } + + // See http://www.xiph.org/vorbis/doc/v-comment.html + // http://age.hobba.nl/audio/mirroredpages/ogg-tagging.html + $metadataMap = array( + 'title' => 'ObjectName', + 'artist' => 'Artist', + 'performer' => 'Artist', + 'description' => 'ImageDescription', + 'license' => 'UsageTerms', + 'copyright' => 'Copyright', + 'organization' => 'dc-publisher', + 'date' => 'DateTimeDigitized', + 'location' => 'LocationDest', + 'contact' => 'Contact', + 'encoded_using' => 'Software', + 'encoder' => 'Software', + // OpenSubtitles.org hash. Identifies source video. + 'source_ohash' => 'OriginalDocumentID', + 'comment' => 'UserComment', + 'language' => 'LanguageCode', + ); + + $props = array(); + + foreach( $metadata['streams'] as $stream ) { + if ( isset( $stream['vendor'] ) ) { + if ( !isset( $props['Software'] ) ) { + $props['Software'] = array(); + } + $props['Software'][] = trim( $stream['vendor'] ); + } + if ( !isset( $stream['comments'] ) ) { + continue; + } + foreach( $stream['comments'] as $name => $rawValue ) { + // $value will be an array if the file has + // a multiple tags with the same name. Otherwise it + // is a string. + foreach( (array) $rawValue as $value ) { + $trimmedValue = trim( $value ); + if ( $trimmedValue === '' ) { + continue; + } + $lowerName = strtolower( $name ); + if ( isset( $metadataMap[$lowerName] ) ) { + $convertedName = $metadataMap[$lowerName]; + if ( !isset( $props[$convertedName] ) ) { + $props[$convertedName] = array(); + } + $props[$convertedName][] = $trimmedValue; + } + } + } + + } + // properties might be duplicated across streams + foreach( $props as &$type ) { + $type = array_unique( $type ); + $type = array_values( $type ); + } + + return $props; + } + + /** + * Get the "media size" + * + * @param $file File + * @param $path string + * @param $metadata bool + * @return array|bool + */ + function getImageSize( $file, $path, $metadata = false ) { + global $wgMediaVideoTypes; + // Just return the size of the first video stream + if ( $metadata === false ) { + $metadata = $file->getMetadata(); + } + $metadata = $this->unpackMetadata( $metadata ); + if ( isset( $metadata['error'] ) || !isset( $metadata['streams'] ) ) { + return false; + } + foreach ( $metadata['streams'] as $stream ) { + if ( in_array( $stream['type'], $wgMediaVideoTypes ) ) { + $pictureWidth = $stream['header']['PICW']; + $parNumerator = $stream['header']['PARN']; + $parDenominator = $stream['header']['PARD']; + if( $parNumerator && $parDenominator ) { + // Compensate for non-square pixel aspect ratios + $pictureWidth = $pictureWidth * $parNumerator / $parDenominator; + } + return array( + intval( $pictureWidth ), + intval( $stream['header']['PICH'] ) + ); + } + } + return array( false, false ); + } + + /** + * @param $metadata + * @return bool|mixed + */ + function unpackMetadata( $metadata ) { + wfSuppressWarnings(); + $unser = unserialize( $metadata ); + wfRestoreWarnings(); + if ( isset( $unser['version'] ) && $unser['version'] == self::METADATA_VERSION ) { + return $unser; + } else { + return false; + } + } + + /** + * @param $image + * @return string + */ + function getMetadataType( $image ) { + return 'ogg'; + } + /** + * @param $file File + */ + function getWebType( $file ) { + $baseType = ( $file->getWidth() == 0 && $file->getHeight() == 0 )? 'audio' : 'video'; + $baseType .= '/ogg'; + $streamTypes = $this->getStreamTypes( $file ); + if ( !$streamTypes ) { + return $baseType; + } + $codecs = strtolower( implode( ", ", $streamTypes ) ); + return $baseType . '; codecs="' . $codecs . '"'; + } + /** + * @param $file File + * @return array|bool + */ + function getStreamTypes( $file ) { + $streamTypes = array(); + $metadata = $this->unpackMetadata( $file->getMetadata() ); + if ( !$metadata || isset( $metadata['error'] ) ) { + return false; + } + foreach ( $metadata['streams'] as $stream ) { + $streamTypes[] = $stream['type']; + } + return array_unique( $streamTypes ); + } + + /** + * @param $file File + * @return int + */ + function getOffset( $file ){ + $metadata = $this->unpackMetadata( $file->getMetadata() ); + if ( !$metadata || isset( $metadata['error'] ) || !isset( $metadata['offset']) ) { + return 0; + } else { + return $metadata['offset']; + } + } + + /** + * @param $file File + * @return int + */ + function getLength( $file ) { + $metadata = $this->unpackMetadata( $file->getMetadata() ); + if ( !$metadata || isset( $metadata['error'] ) ) { + return 0; + } else { + return $metadata['length']; + } + } + + /** + * Get useful response headers for GET/HEAD requests for a file with the given metadata + * @param $metadata mixed Result this handlers getMetadata() for a file + * @return Array + */ + public function getStreamHeaders( $metadata ) { + $metadata = $this->unpackMetadata( $metadata ); + if ( $metadata && !isset( $metadata['error'] ) && isset( $metadata['length'] ) ) { + return array( 'X-Content-Duration' => floatval( $metadata[ 'length' ] ) ); + } + return array(); + } + + /** + * @param $file File + * @return float|int + */ + function getFramerate( $file ){ + $metadata = $this->unpackMetadata( $file->getMetadata() ); + if ( !$metadata || isset( $metadata['error'] ) ) { + return 0; + } else { + // Return the first found theora stream framerate: + foreach ( $metadata['streams'] as $stream ) { + if( $stream['type'] == 'Theora' ){ + return $stream['header']['FRN'] / $stream['header']['FRD']; + } + } + return 0; + } + } + + /** + * @param $file File + * @return String + */ + function getShortDesc( $file ) { + global $wgLang, $wgMediaAudioTypes, $wgMediaVideoTypes; + + $streamTypes = $this->getStreamTypes( $file ); + if ( !$streamTypes ) { + return parent::getShortDesc( $file ); + } + if ( array_intersect( $streamTypes, $wgMediaVideoTypes ) ) { + // Count multiplexed audio/video as video for short descriptions + $msg = 'timedmedia-ogg-short-video'; + } elseif ( array_intersect( $streamTypes, $wgMediaAudioTypes ) ) { + $msg = 'timedmedia-ogg-short-audio'; + } else { + $msg = 'timedmedia-ogg-short-general'; + } + return wfMessage( $msg, implode( '/', $streamTypes ), + $wgLang->formatTimePeriod( $this->getLength( $file ) ) )->text(); + } + + /** + * @param $file File + * @return String + */ + function getLongDesc( $file ) { + global $wgLang, $wgMediaVideoTypes, $wgMediaAudioTypes; + + $streamTypes = $this->getStreamTypes( $file ); + if ( !$streamTypes ) { + $unpacked = $this->unpackMetadata( $file->getMetadata() ); + if ( isset( $unpacked['error']['message'] ) ) { + return wfMessage( 'timedmedia-ogg-long-error', $unpacked['error']['message'] )->text(); + } else { + return wfMessage( 'timedmedia-ogg-long-no-streams' )->text(); + } + } + if ( array_intersect( $streamTypes,$wgMediaVideoTypes ) ) { + if ( array_intersect( $streamTypes, $wgMediaAudioTypes ) ) { + $msg = 'timedmedia-ogg-long-multiplexed'; + } else { + $msg = 'timedmedia-ogg-long-video'; + } + } elseif ( array_intersect( $streamTypes, $wgMediaAudioTypes ) ) { + $msg = 'timedmedia-ogg-long-audio'; + } else { + $msg = 'timedmedia-ogg-long-general'; + } + $size = 0; + $unpacked = $this->unpackMetadata( $file->getMetadata() ); + if ( !$unpacked || isset( $metadata['error'] ) ) { + $length = 0; + } else { + $length = $this->getLength( $file ); + foreach ( $unpacked['streams'] as $stream ) { + if( isset( $stream['size'] ) ) + $size += $stream['size']; + } + } + return wfMessage( + $msg, + implode( '/', $streamTypes ), + $wgLang->formatTimePeriod( $length ), + $wgLang->formatBitrate( $this->getBitRate( $file ) ) + )->numParams( + $file->getWidth(), + $file->getHeight() + )->text(); + } + + /** + * @param $file File + * @return float|int + */ + function getBitRate( &$file ){ + $size = 0; + $unpacked = $this->unpackMetadata( $file->getMetadata() ); + if ( !$unpacked || isset( $unpacked['error'] ) ) { + $length = 0; + } else { + $length = $this->getLength( $file ); + if ( isset( $unpacked['streams'] ) ) { + foreach ( $unpacked['streams'] as $stream ) { + if( isset( $stream['size'] ) ) + $size += $stream['size']; + } + } + } + return $length == 0 ? 0 : $size / $length * 8; + } +} diff --git a/extensions/TimedMediaHandler/handlers/TextHandler/TextHandler.php b/extensions/TimedMediaHandler/handlers/TextHandler/TextHandler.php new file mode 100644 index 00000000..3c229d5f --- /dev/null +++ b/extensions/TimedMediaHandler/handlers/TextHandler/TextHandler.php @@ -0,0 +1,305 @@ +<?php +/** + * Timed Text handling for mediaWiki + * + * Timed text support is presently fairly limited. Unlike Ogg and WebM handlers, + * timed text does not extend the TimedMediaHandler class. + * + * TODO On "new" timedtext language save purge all pages where file exists + */ + + +/** + * Subclass ApiMain but query other db + */ +class ForeignApiQueryAllPages extends ApiQueryAllPages { + public function __construct( $mDb, $query, $moduleName ) { + global $wgTimedTextForeignNamespaces; + + $this->foreignDb = $mDb; + + $wikiID = $this->foreignDb->getWikiID(); + if ( isset( $wgTimedTextForeignNamespaces[ $wikiID ] ) ) { + $this->foreignNs = $wgTimedTextForeignNamespaces[ $wikiID ]; + } else { + $this->foreignNs = NS_TIMEDTEXT; + } + parent::__construct( $query, $moduleName, 'ap' ); + } + + protected function getDB() { + return $this->foreignDb; + } + + protected function parseMultiValue( $valueName, $value, $allowMultiple, $allowedValues ) { + // foreignnNs might not be defined localy, + // catch the undefined error here + if ( $valueName == 'apnamespace' + && $value == $this->foreignNs + && $allowMultiple == false + ) { + return $this->foreignNs; + } + return parent::parseMultiValue( $valueName, $value, $allowMultiple, $allowedValues ); + } + + /** + * An alternative to titleToKey() that doesn't trim trailing spaces + * + * + * @FIXME: I'M A BIG HACK + * + * @param string $titlePart Title part with spaces + * @return string Title part with underscores + */ + public function titlePartToKey( $titlePart, $defaultNamespace = NS_MAIN ) { + return substr( $this->titleToKey( $titlePart . 'x' ), 0, -1 ); + } +} + +class TextHandler { + var $remoteNs = null;//lazy init remote Namespace number + + /** + * @var File + */ + protected $file; + + function __construct( $file ){ + $this->file = $file; + } + + /** + * Get the timed text tracks elements as an associative array + * @return array|mixed + */ + function getTracks(){ + if( $this->file->isLocal() ){ + return $this->getLocalTextSources(); + } elseif ( $this->file->getRepo() instanceof ForeignDBViaLBRepo ){ + return $this->getForeignDBTextSources(); + } else { + return $this->getRemoteTextSources(); + } + } + + /** + * @return bool|int|null + */ + function getTimedTextNamespace(){ + global $wgEnableLocalTimedText; + if( $this->file->isLocal() ) { + if ( $wgEnableLocalTimedText ) { + return NS_TIMEDTEXT; + } else { + return false; + } + } elseif( $this->file->repo instanceof ForeignDBViaLBRepo ){ + global $wgTimedTextForeignNamespaces; + $wikiID = $this->file->getRepo()->getSlaveDB()->getWikiID(); + if ( isset( $wgTimedTextForeignNamespaces[ $wikiID ] ) ) { + return $wgTimedTextForeignNamespaces[ $wikiID ]; + } + // failed to get namespace via ForeignDBViaLBRepo, return NS_TIMEDTEXT + return NS_TIMEDTEXT; + } else { + if( $this->remoteNs !== null ){ + return $this->remoteNs; + } + // Get the namespace data from the image api repo: + // fetchImageQuery query caches results + $data = $this->file->getRepo()->fetchImageQuery( array( + 'meta' =>'siteinfo', + 'siprop' => 'namespaces' + )); + + if( isset( $data['query'] ) && isset( $data['query']['namespaces'] ) ){ + // get the ~last~ timed text namespace defined + foreach( $data['query']['namespaces'] as $ns ){ + if( $ns['*'] == 'TimedText' ){ + $this->remoteNs = $ns['id']; + } + } + } + // Return the remote Ns + return $this->remoteNs; + } + } + + /** + * @return array|bool + */ + function getTextPagesQuery(){ + $ns = $this->getTimedTextNamespace(); + if( $ns === false ){ + wfDebug("Repo: " . $this->file->repo->getName() . " does not have a TimedText namesapce \n"); + // No timed text namespace, don't try to look up timed text tracks + return false; + } + return array( + 'action' => 'query', + 'list' => 'allpages', + 'apnamespace' => $ns, + 'aplimit' => 300, + 'apprefix' => $this->file->getTitle()->getDBkey() + ); + } + + /** + * @return array|mixed + */ + function getRemoteTextSources(){ + global $wgMemc; + // Use descriptionCacheExpiry as our expire for timed text tracks info + if ( $this->file->getRepo()->descriptionCacheExpiry > 0 ) { + wfDebug("Attempting to get text tracks from cache..."); + $key = $this->file->getRepo()->getLocalCacheKey( 'RemoteTextTracks', 'url', $this->file->getName() ); + $obj = $wgMemc->get($key); + if ($obj) { + wfDebug("success!\n"); + return $obj; + } + wfDebug("miss\n"); + } + wfDebug("Get text tracks from remote api \n"); + $query = $this->getTextPagesQuery(); + + // Error in getting timed text namespace return empty array; + if( $query === false ){ + return array(); + } + $data = $this->file->getRepo()->fetchImageQuery( $query ); + $textTracks = $this->getTextTracksFromData( $data ); + if ( $data && $this->file->repo->descriptionCacheExpiry > 0 ) { + $wgMemc->set( $key, $textTracks, $this->file->repo->descriptionCacheExpiry ); + } + return $textTracks; + } + + /** + * @return array + */ + function getLocalTextSources(){ + global $wgEnableLocalTimedText; + if ( $wgEnableLocalTimedText ) { + // Init $this->textTracks + $params = new FauxRequest( $this->getTextPagesQuery() ); + $api = new ApiMain( $params ); + $api->execute(); + if ( defined( 'ApiResult::META_CONTENT' ) ) { + $data = $api->getResult()->getResultData( null, array( 'Strip' => 'all' ) ); + } else { + $data = $api->getResultData(); + } + wfDebug(print_r($data, true)); + // Get the list of language Names + return $this->getTextTracksFromData( $data ); + } else { + return array(); + } + } + + /** + * @return array|mixed + */ + function getForeignDBTextSources(){ + // Init $this->textTracks + $params = new FauxRequest( $this->getTextPagesQuery() ); + $api = new ApiMain( $params ); + $api->profileIn(); + $query = new ApiQuery( $api, 'foo', 'bar' ); + $query->profileIn(); + $module = new ForeignApiQueryAllPages( $this->file->getRepo()->getSlaveDB(), $query, 'allpages' ); + $module->profileIn(); + $module->execute(); + $module->profileOut(); + $query->profileOut(); + $api->profileOut(); + + if ( defined( 'ApiResult::META_CONTENT' ) ) { + $data = $module->getResult()->getResultData( null, array( 'Strip' => 'all' ) ); + } else { + $data = $module->getResultData(); + } + // Get the list of language Names + return $this->getTextTracksFromData( $data ); + } + + /** + * @param $data + * @return array + */ + function getTextTracksFromData( $data ){ + $textTracks = array(); + $providerName = $this->file->repo->getName(); + // commons is called shared in production. normalize it to wikimediacommons + if( $providerName == 'shared' ){ + $providerName = 'wikimediacommons'; + } + // Provider name should be the same as the interwiki map + // @@todo more testing with this: + + $langNames = Language::fetchLanguageNames( null, 'mw' ); + if( $data['query'] && $data['query']['allpages'] ){ + foreach( $data['query']['allpages'] as $page ){ + $subTitle = Title::newFromText( $page['title'] ) ; + $tileParts = explode( '.', $page['title'] ); + if( count( $tileParts) >= 3 ){ + $timedTextExtension = array_pop( $tileParts ); + $languageKey = array_pop( $tileParts ); + $contentType = $this->getContentType( $timedTextExtension ); + } else { + continue; + } + // If there is no valid language continue: + if( !isset( $langNames[ $languageKey ] ) ){ + continue; + } + $namespacePrefix = "TimedText:"; + $textTracks[] = array( + 'kind' => 'subtitles', + 'data-mwtitle' => $namespacePrefix . $subTitle->getDBkey(), + 'data-mwprovider' => $providerName, + 'type' => $contentType, + // @todo Should eventually add special entry point and output proper WebVTT format: + // http://www.whatwg.org/specs/web-apps/current-work/webvtt.html + 'src' => $this->getFullURL( $page['title'], $contentType ), + 'srclang' => $languageKey, + 'data-dir' => Language::factory( $languageKey )->getDir(), + 'label' => wfMessage('timedmedia-subtitle-language', + $langNames[ $languageKey ], + $languageKey )->text() + ); + } + } + return $textTracks; + } + + function getContentType( $timedTextExtension ) { + if ( $timedTextExtension === 'srt' ) { + return 'text/x-srt'; + } else if ( $timedTextExtension === 'vtt' ) { + return 'text/vtt'; + } + return ''; + } + + function getFullURL( $pageTitle, $contentType ){ + if( $this->file->isLocal() ) { + $subTitle = Title::newFromText( $pageTitle ) ; + return $subTitle->getFullURL( array( + 'action' => 'raw', + 'ctype' => $contentType + )); + //} elseif( $this->file->repo instanceof ForeignDBViaLBRepo ){ + } else { + $query = 'title=' . wfUrlencode( $pageTitle ) . '&'; + $query .= wfArrayToCgi( array( + 'action' => 'raw', + 'ctype' => $contentType + ) ); + // Note: This will return false if scriptDirUrl is not set for repo. + return $this->file->repo->makeUrl( $query ); + } + } +} diff --git a/extensions/TimedMediaHandler/handlers/WAVHandler/WAVHandler.php b/extensions/TimedMediaHandler/handlers/WAVHandler/WAVHandler.php new file mode 100644 index 00000000..9fae12b8 --- /dev/null +++ b/extensions/TimedMediaHandler/handlers/WAVHandler/WAVHandler.php @@ -0,0 +1,87 @@ +<?php +/** + * WAV handler + */ +class WAVHandler extends ID3Handler { + + /** + * @param $file File + * @return string + */ + function getMetadataType( $file ) { + return 'wav'; + } + + /** + * @param $file File + * @return String + */ + function getWebType( $file ) { + return 'audio/wav'; + } + + function verifyUpload( $filename ) { + $metadata = $this->getID3( $filename ); + + if( + isset( $metadata['audio'] ) + && $metadata['audio']['dataformat'] == 'wav' + && ( $metadata['audio']['codec'] == 'Pulse Code Modulation (PCM)' || $metadata['audio']['codec'] == 'IEEE Float' ) + ){ + return Status::newGood(); + } + + return Status::newFatal( 'timedmedia-wav-pcm-required' ); + } + /** + * @param $file File + * @return array|bool + */ + function getStreamTypes( $file ) { + $streamTypes = array(); + $metadata = $this->unpackMetadata( $file->getMetadata() ); + + if ( !$metadata || isset( $metadata['error'] ) ) { + return false; + } + + if( isset( $metadata['audio'] ) && $metadata['audio']['dataformat'] == 'wav' ){ + $streamTypes[] = 'WAV'; + } + + return $streamTypes; + } + + /** + * @param $file File + * @return String + */ + function getShortDesc( $file ) { + global $wgLang; + + $streamTypes = $this->getStreamTypes( $file ); + if ( !$streamTypes ) { + return parent::getShortDesc( $file ); + } + return wfMessage( 'timedmedia-wav-short-audio', + $wgLang->formatTimePeriod( $this->getLength( $file ) ) )->text(); + } + + /** + * @param $file File + * @return String + */ + function getLongDesc( $file ) { + global $wgLang; + $streamTypes = $this->getStreamTypes( $file ); + if ( !$streamTypes ) { + return parent::getLongDesc( $file ); + } + return wfMessage('timedmedia-wav-long-audio', + $wgLang->formatTimePeriod( $this->getLength($file) ), + $wgLang->formatBitrate( $this->getBitRate( $file ) ) + )->text(); + + } + +} diff --git a/extensions/TimedMediaHandler/handlers/WebMHandler/WebMHandler.php b/extensions/TimedMediaHandler/handlers/WebMHandler/WebMHandler.php new file mode 100644 index 00000000..ebdad0d1 --- /dev/null +++ b/extensions/TimedMediaHandler/handlers/WebMHandler/WebMHandler.php @@ -0,0 +1,175 @@ +<?php +/** + * WebM handler + */ +class WebMHandler extends ID3Handler { + + /** + * @param $path string + * @return array + */ + protected function getID3( $path ) { + $id3 = parent::getID3( $path ); + // Unset some parts of id3 that are too detailed and matroska specific: + unset( $id3['matroska'] ); + return $id3; + } + + /** + * Get the "media size" + * @param $file File + * @param $path string + * @param $metadata bool + * @return array|bool + */ + function getImageSize( $file, $path, $metadata = false ) { + // Just return the size of the first video stream + if ( $metadata === false ) { + $metadata = $file->getMetadata(); + } + $metadata = $this->unpackMetadata( $metadata ); + if ( isset( $metadata['error'] ) ) { + return false; + } + + $size = array( false, false ); + // display_x/display_y is only set if DisplayUnit + // is pixels, otherwise display_aspect_ratio is set + if ( isset( $metadata['video']['display_x'] ) + && + isset( $metadata['video']['display_y'] ) + ){ + $size = array ( + $metadata['video']['display_x'], + $metadata['video']['display_y'] + ); + } + elseif ( isset( $metadata['video']['resolution_x'] ) + && + isset( $metadata['video']['resolution_y'] ) + ){ + $size = array ( + $metadata['video']['resolution_x'], + $metadata['video']['resolution_y'] + ); + if ( isset($metadata['video']['crop_top']) ) { + $size[1] -= $metadata['video']['crop_top']; + } + if ( isset($metadata['video']['crop_bottom']) ) { + $size[1] -= $metadata['video']['crop_bottom']; + } + if ( isset($metadata['video']['crop_left']) ) { + $size[0] -= $metadata['video']['crop_left']; + } + if ( isset($metadata['video']['crop_right']) ) { + $size[0] -= $metadata['video']['crop_right']; + } + } + if ( $size[0] && $size[1] && isset( $metadata['video']['display_aspect_ratio'] ) ) { + //for wide images (i.e. 16:9) take native height as base + if ( $metadata['video']['display_aspect_ratio'] >= 1 ) { + $size[0] = intval( $size[1] * $metadata['video']['display_aspect_ratio'] ); + } else { //for tall images (i.e. 9:16) take width as base + $size[1] = intval( $size[0] / $metadata['video']['display_aspect_ratio'] ); + } + } + return $size; + } + + /** + * @param $file + * @return string + */ + function getMetadataType( $file ) { + return 'webm'; + } + + /** + * @param $file File + * @return String + */ + function getWebType( $file ) { + $baseType = ( $file->getWidth() == 0 && $file->getHeight() == 0 )? 'audio' : 'video'; + + $streams = $this->getStreamTypes( $file ); + if ( !$streams ) { + return $baseType . '/webm'; + } + + $codecs = strtolower( implode( ', ', $streams ) ); + + return $baseType . '/webm; codecs="' . $codecs . '"'; + } + + /** + * @param $file File + * @return array|bool + */ + function getStreamTypes( $file ) { + $streamTypes = array(); + $metadata = $this->unpackMetadata( $file->getMetadata() ); + if ( !$metadata || isset( $metadata['error'] ) ) { + return false; + } + // id3 gives 'V_VP8' for what we call VP8 + if( isset( $metadata['video'] ) && $metadata['video']['dataformat'] == 'vp8' ){ + $streamTypes[] = 'VP8'; + } elseif( isset( $metadata['video'] ) && + ( $metadata['video']['dataformat'] === 'vp9' + || $metadata['video']['dataformat'] === 'V_VP9' + ) ) { + // Currently getID3 calls it V_VP9. That will probably change to vp9 + // once getID3 actually gets support for the codec. + $streamTypes[] = 'VP9'; + } + if( isset( $metadata['audio'] ) && $metadata['audio']['dataformat'] == 'vorbis' ){ + $streamTypes[] = 'Vorbis'; + } elseif ( isset( $metadata['audio'] ) && + ( $metadata['audio']['dataformat'] == 'opus' + || $metadata['audio']['dataformat'] == 'A_OPUS' + ) ) { + // Currently getID3 calls it A_OPUS. That will probably change to 'opus' + // once getID3 actually gets support for the codec. + $streamTypes[] = 'Opus'; + } + + return $streamTypes; + } + + /** + * @param $file File + * @return String + */ + function getShortDesc( $file ) { + global $wgLang; + + $streamTypes = $this->getStreamTypes( $file ); + if ( !$streamTypes ) { + return parent::getShortDesc( $file ); + } + return wfMessage( 'timedmedia-webm-short-video', implode( '/', $streamTypes ), + $wgLang->formatTimePeriod( $this->getLength( $file ) ) )->text(); + } + + /** + * @param $file File + * @return String + */ + function getLongDesc( $file ) { + global $wgLang; + $streamTypes = $this->getStreamTypes( $file ); + if ( !$streamTypes ) { + return parent::getLongDesc( $file ); + } + return wfMessage('timedmedia-webm-long-video', + implode( '/', $streamTypes ), + $wgLang->formatTimePeriod( $this->getLength($file) ), + $wgLang->formatBitrate( $this->getBitRate( $file ) ) + )->numParams( + $file->getWidth(), + $file->getHeight() + )->text(); + + } + +} |