├── .travis.yml ├── LICENSE ├── README.md ├── _config.yml ├── benchmark.php ├── bin └── mediafile ├── composer.json ├── phpunit.xml.dist ├── src ├── Adapters │ ├── Audio │ │ ├── AacAdapter.php │ │ ├── AmrAdapter.php │ │ ├── FlacAdapter.php │ │ ├── Mp3Adapter.php │ │ ├── OggAdapter.php │ │ ├── WavAdapter.php │ │ └── WmaAdapter.php │ ├── AudioAdapter.php │ ├── ContainerAdapter.php │ ├── Containers │ │ ├── AsfAdapter.php │ │ ├── MatroskaContainer.php │ │ └── Mpeg4Part12Adapter.php │ ├── Video │ │ ├── AviAdapter.php │ │ ├── MkvAdapter.php │ │ ├── Mp4Adapter.php │ │ └── WmvAdapter.php │ └── VideoAdapter.php ├── Exceptions │ ├── Exception.php │ ├── FileAccessException.php │ └── ParsingException.php └── MediaFile.php └── tests ├── audios └── AacAudioTest.php └── bootstrap.php /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | dist: trusty 3 | php: 4 | - 5.5 5 | - 5.6 6 | - 7.0 7 | - 7.1 8 | - hhvm 9 | 10 | before_script: 11 | - composer install 12 | 13 | script: 14 | - vendor/bin/phpunit 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MediaFile 2 | 3 | Allows you easily get meta information about any media file with unified interface. 4 | The library has no requirements of external libs or system unitilies. 5 | 6 | [![Latest Stable Version](https://poser.pugx.org/wapmorgan/media-file/v/stable)](https://packagist.org/packages/wapmorgan/media-file) 7 | [![License](https://poser.pugx.org/wapmorgan/media-file/license)](https://packagist.org/packages/wapmorgan/media-file) 8 | [![Latest Unstable Version](https://poser.pugx.org/wapmorgan/media-file/v/unstable)](https://packagist.org/packages/wapmorgan/media-file) 9 | [![Tests](https://travis-ci.org/wapmorgan/MediaFile.svg?branch=master)](https://travis-ci.org/wapmorgan/MediaFile) 10 | 11 | ## Supported formats 12 | 13 | | Audio | Video | 14 | |-------|-------| 15 | | aac, amr, flac, mp3, ogg, wav, wma | avi, mkv, mp4, wmv | 16 | | - length | - length | 17 | | - bitRate | - width | 18 | | - sampleRate | - height | 19 | | - channels | - frameRate | 20 | 21 | **Table of contents:** 22 | 1. Usage 23 | 2. API 24 | 3. Why not using getID3? 25 | 4. Technical details 26 | 27 | ## Usage 28 | 29 | ```php 30 | use wapmorgan\MediaFile\MediaFile; 31 | 32 | try { 33 | $media = MediaFile::open('123.mp3'); 34 | // for audio 35 | if ($media->isAudio()) { 36 | $audio = $media->getAudio(); 37 | echo 'Duration: '.$audio->getLength().PHP_EOL; 38 | echo 'Bit rate: '.$audio->getBitRate().PHP_EOL; 39 | echo 'Sample rate: '.$audio->getSampleRate().PHP_EOL; 40 | echo 'Channels: '.$audio->getChannels().PHP_EOL; 41 | } 42 | // for video 43 | else { 44 | $video = $media->getVideo(); 45 | // calls to VideoAdapter interface 46 | echo 'Duration: '.$video->getLength().PHP_EOL; 47 | echo 'Dimensions: '.$video->getWidth().'x'.$video->getHeight().PHP_EOL; 48 | echo 'Framerate: '.$video->getFramerate().PHP_EOL; 49 | } 50 | } catch (wapmorgan\MediaFile\Exceptions\FileAccessException $e) { 51 | // FileAccessException throws when file is not a detected media 52 | } catch (wapmorgan\MediaFile\Exceptions\ParsingException $e) { 53 | echo 'File is propably corrupted: '.$e->getMessage().PHP_EOL; 54 | } 55 | ``` 56 | 57 | ## API 58 | ### MediaFile 59 | 60 | `wapmorgan\wapmorgan\MediaFile` 61 | 62 | | Method | Description | Notes | 63 | |------------------------------------------|-----------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------| 64 | | `static open($filename): MediaFile` | Detects file type and format and calls constructor with these parameters. | Throws an `\Exception` if file is not a media or is not accessible. | 65 | | `isAudio(): boolean` | Returns true if media is just audio. | | 66 | | `isVideo(): boolean` | Returns true if media is a video with audio. | | 67 | | `isContainer(): boolean` | Returns true if media is also a container (can store multiple audios and videos). | | 68 | | `getFormat(): string` | Returns media file format. | | 69 | | `getAudio(): AudioAdapter` | Returns an `AudioAdapter` interface for audio. | | 70 | | `getVideo(): VideoAdapter` | Returns an `VideoAdapter` interface for video. | | 71 | 72 | ### AudioAdapter 73 | 74 | `wapmorgan\MediaFile\AudioAdapter` 75 | 76 | | Method | Description | 77 | |--------------------------------|-------------------------------------------------------------------| 78 | | `getLength(): float` | Returns audio length in seconds and microseconds as _float_. | 79 | | `getBitRate(): int` | Returns audio bit rate as _int_. | 80 | | `getSampleRate(): int` | Returns audio sampling rate as _int_. | 81 | | `getChannels(): int` | Returns number of channels used in audio as _int_. | 82 | | `isVariableBitRate(): boolean` | Returns whether format support VBR and file has VBR as _boolean_. | 83 | | `isLossless(): boolean` | Returns whether format has compression lossless as _boolean_. | 84 | 85 | ### VideoAdapter 86 | 87 | `wapmorgan\MediaFile\VideoAdapter` 88 | 89 | | Method | Description | 90 | |-----------------------|--------------------------------------------------------------| 91 | | `getLength(): int` | Returns video length in seconds and microseconds as _float_. | 92 | | `getWidth(): int` | Returns width of video as _int_. | 93 | | `getHeight(): int` | Returns height of video as _int_. | 94 | | `getFramerate(): int` | Returns video frame rate of video as _int_. | 95 | 96 | ### ContainerAdapter 97 | 98 | `wapmorgan\MediaFile\ContainerAdapter` 99 | 100 | | Method | Description | 101 | |----------------------------|--------------------------------------------------| 102 | | `countStreams(): int` | Returns number of streams in container as _int_. | 103 | | `countVideoStreams(): int` | Returns number of video streams as _int_. | 104 | | `countAudioStreams(): int` | Returns number of audio streams as _int_. | 105 | | `getStreams(): array` | Returns streams information as _array_. | 106 | 107 | ## Why not using getID3? 108 | 109 | getID3 library is very popular and has a lot of features, but it's old and too slow. 110 | 111 | Following table shows comparation of analyzing speed of fixtures, distributed with first release of MediaFile: 112 | 113 | | File | getID3 | MediaFile | Speed gain | 114 | |------------|--------|-----------|------------| 115 | | video.avi | 0.215 | 0.126 | 1.71x | 116 | | video.mp4 | 3.055 | 0.429 | 7.12x | 117 | | video.wmv | 0.354 | 0.372 | 0.95x | 118 | | audio.aac | 0.560 | 0.262 | 2.13x | 119 | | audio.amr | 8.241 | 12.248 | 0.67x | 120 | | audio.flac | 1.880 | 0.071 | 26.41x | 121 | | audio.m4a | 13.372 | 0.169 | 79.14x | 122 | | audio.mp3 | 10.931 | 0.077 | 141.54x | 123 | | audio.ogg | 0.170 | 0.096 | 1.78x | 124 | | audio.wav | 0.114 | 0.070 | 1.64x | 125 | | audio.wma | 0.195 | 0.158 | 1.23x | 126 | 127 | ## Technical information 128 | 129 | | Format | Full format name | Specifications | Notes | 130 | |--------|--------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------| 131 | | aac | MPEG 4 Part 12 container with audio only | http://l.web.umkc.edu/lizhu/teaching/2016sp.video-communication/ref/mp4.pdf | Does not provide support of MPEG2-AAC | 132 | | amr | AMR-NB | http://hackipedia.org/File%20formats/Containers/AMR,%20Adaptive%20MultiRate/AMR%20format.pdf | Does not provide support of AMR-WB | 133 | | avi | - | http://www.alexander-noe.com/video/documentation/avi.pdf | | 134 | | flac | - | - | Support based on third-party library | 135 | | mkv | Matroska container | https://www.matroska.org/technical/specs/index.html | | 136 | | mp3 | MPEG 1/2 Layer 1/2/3 | https://github.com/wapmorgan/mp3info#technical-information | | 137 | | mp4 | MPEG 4 Part 12/14 container with few audio and video streams | Part 12 specification: http://l.web.umkc.edu/lizhu/teaching/2016sp.video-communication/ref/mp4.pdf Part 14 extension: https://www.cmlab.csie.ntu.edu.tw/~cathyp/eBooks/14496_MPEG4/ISO_IEC_14496-14_2003-11-15.pdf | | 138 | | ogg | Ogg container with Vorbis audio | https://xiph.org/vorbis/doc/Vorbis_I_spec.html | | 139 | | wav | - | - | Support based on third-party library | 140 | | wma | ASF container with only one audio stream | http://go.microsoft.com/fwlink/p/?linkid=31334 | | 141 | | wmv | ASF container with few audio and video streams | http://go.microsoft.com/fwlink/p/?linkid=31334 | | 142 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-cayman 2 | -------------------------------------------------------------------------------- /benchmark.php: -------------------------------------------------------------------------------- 1 | array(), 16 | 'getid3' => array(), 17 | ); 18 | 19 | echo 'Repeatitions: '.REPEATITIONS.PHP_EOL; 20 | 21 | echo sprintf('%20s | %10s | %10s | %10s', 'File', 'getID3', 'MediaFile', 'Speed gain').PHP_EOL; 22 | 23 | foreach ($files as $file) { 24 | $start = microtime(true); 25 | try { 26 | for ($i = 0; $i < REPEATITIONS; $i++) { 27 | $info = MediaFile::open($file); 28 | } 29 | $times['mediafile'][$file] = microtime(true) - $start; 30 | } catch (FileAccessException $e) { 31 | continue; 32 | } 33 | 34 | $start = microtime(true); 35 | for ($i = 0; $i < REPEATITIONS; $i++) { 36 | $info = $id3->analyze($file); 37 | } 38 | $times['getid3'][$file] = microtime(true) - $start; 39 | 40 | echo sprintf('%20s | %10.3f | %10.3f | %5.2fx', basename($file), $times['getid3'][$file], $times['mediafile'][$file], $times['getid3'][$file] / $times['mediafile'][$file]).PHP_EOL; 41 | } 42 | // var_dump($times); 43 | -------------------------------------------------------------------------------- /bin/mediafile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/php 2 | '.PHP_EOL); 9 | array_shift($argv); 10 | 11 | if (in_array('--debug', $argv)) { 12 | define('DEBUG', true); 13 | unset($argv[array_search('--debug', $argv)]); 14 | } 15 | 16 | function substrIfLonger($string, $maxLength) 17 | { 18 | return (strlen($string) > $maxLength) ? substr($string, 0, $maxLength - 2).'..' : $string; 19 | } 20 | 21 | function scanFile($file) { 22 | try { 23 | $media = MediaFile::open($file); 24 | if ($media->isAudio()) { 25 | $audio = $media->getAudio(); 26 | $length = $audio->getLength(); 27 | echo sprintf('%-30s | %5s | %3d:%02d | %8d | %10.1f | %d', 28 | substrIfLonger($file, 30), 29 | $media->getFormat(), 30 | floor($length / 60), 31 | floor($length % 60), 32 | $media->getAudio()->getBitRate() / 1000, 33 | $media->getAudio()->getSampleRate() / 1000, 34 | $media->getAudio()->getChannels()).PHP_EOL; 35 | } else if ($media->isVideo()) { 36 | $length = $media->getVideo()->getLength(); 37 | echo $file.':'.$media->getFormat().' '.sprintf('%d:%02d', floor($length / 60), floor($length % 60)).' '.$media->getVideo()->getWidth().'x'.$media->getVideo()->getHeight().' '.$media->getVideo()->getFrameRate().'fps'.PHP_EOL; 38 | } 39 | } catch (Exception $e) { 40 | if ($e instanceof ParsingException) 41 | echo 'File is propably corrupted: '.$e->getMessage().PHP_EOL; 42 | } 43 | } 44 | 45 | function scanDirectory($dir) 46 | { 47 | foreach (glob(trim($dir, '/*').'/*') as $file) { 48 | if (is_dir($file)) 49 | scanDirectory($file); 50 | else if (is_file($file)) 51 | scanFile($file); 52 | } 53 | } 54 | 55 | function scan($points) 56 | { 57 | foreach ($points as $file) { 58 | if (is_dir($file)) { 59 | scanDirectory($file); 60 | } else if (is_file($file)) 61 | scanFile($file); 62 | } 63 | } 64 | 65 | echo sprintf('%-30s | %5s | %5s | %8s | %10s | %s', 'File name', 'Format', 'Length', 'BitRate', 'SampleRate', 'Channels').PHP_EOL; 66 | echo sprintf('%-30s | %5s | %5s | %s | %s', 'File name', 'Format', 'Length', 'Resolution', 'fps').PHP_EOL; 67 | scan($argv); 68 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wapmorgan/media-file", 3 | "description": "A unified reader of metadata from audio & video files", 4 | "license": "MIT", 5 | "keywords": ["wav", "flac", "aac", "ogg", "mp3", "amr", "avi", "wma", "wmv", "mp4"], 6 | "autoload": { 7 | "psr-4": { 8 | "wapmorgan\\MediaFile\\": "src/" 9 | } 10 | }, 11 | "require": { 12 | "php": ">=5.5", 13 | "bluemoehre/flac-php": "1.0.2", 14 | "wapmorgan/mp3info": "~0.0", 15 | "wapmorgan/binary-stream": "~0.4.0", 16 | "wapmorgan/file-type-detector": "^1.0.2", 17 | "boyhagemann/wave": "dev-master" 18 | }, 19 | "suggest": { 20 | "boyhagemann/wave": "Wav support" 21 | }, 22 | "require-dev": { 23 | "phpunit/phpunit": "~4.8", 24 | "james-heinrich/getid3": "^1.9" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | tests/ 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/Adapters/Audio/AacAdapter.php: -------------------------------------------------------------------------------- 1 | mvhd['duration'] / $this->mvhd['timescale']; 14 | } 15 | 16 | public function getBitRate() { 17 | return floor($this->mdat['size'] / ($this->mvhd['duration'] / $this->mvhd['timescale']) * 8); 18 | } 19 | 20 | public function getSampleRate() { 21 | foreach ($this->streams as $stream) 22 | return $stream['sample_rate']; 23 | } 24 | 25 | public function getChannels() { 26 | foreach ($this->streams as $stream) 27 | return $stream['channels']; 28 | } 29 | 30 | public function isVariableBitRate() { 31 | return false; 32 | } 33 | 34 | public function isLossless() { 35 | return false; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Adapters/Audio/AmrAdapter.php: -------------------------------------------------------------------------------- 1 | 4750, 20 | 1 => 5150, 21 | 2 => 5900, 22 | 3 => 6700, 23 | 4 => 7400, 24 | 5 => 7950, 25 | 6 => 10200, 26 | 7 => 12200, 27 | ); 28 | 29 | static protected $frameSizes = array( 30 | 0 => 13, 31 | 1 => 14, 32 | 2 => 16, 33 | 3 => 18, 34 | 4 => 20, 35 | 5 => 21, 36 | 6 => 27, 37 | 7 => 32, 38 | ); 39 | 40 | /** 41 | * AmrAdapter constructor. 42 | * 43 | * @param $filename 44 | * 45 | * @throws \wapmorgan\MediaFile\Exceptions\FileAccessException 46 | * @throws \wapmorgan\MediaFile\Exceptions\ParsingException 47 | */ 48 | public function __construct($filename) { 49 | if (!file_exists($filename) || !is_readable($filename)) throw new FileAccessException('File "'.$filename.'" is not available for reading!'); 50 | $this->filename = $filename; 51 | $this->stream = new BinaryStream($filename); 52 | $this->stream->saveGroup('frame', array( 53 | '_' => 1, 54 | 'mode' => 3, 55 | '__' => 4, 56 | )); 57 | $this->scan(); 58 | } 59 | 60 | /** 61 | * @throws \wapmorgan\MediaFile\Exceptions\ParsingException 62 | */ 63 | protected function scan() { 64 | if (!$this->stream->compare(5, '#!AMR')) 65 | throw new ParsingException('File is not an amr file!'); 66 | $this->stream->skip(6); 67 | 68 | $bitrates = array(); 69 | $frames = 0; 70 | while (!$this->stream->isEnd()) { 71 | $frames++; 72 | $frame = $this->stream->readGroup('frame'); 73 | if ($this->stream->isEnd()) 74 | break; 75 | $bitrate = self::$modes[$frame['mode']]; 76 | if (isset($bitrates[$bitrate])) $bitrates[$bitrate]++; 77 | else $bitrates[$bitrate] = 1; 78 | $this->stream->skip(self::$frameSizes[$frame['mode']] - 1); 79 | } 80 | $this->bitrates = $bitrates; 81 | $this->length = 0.02 * $frames; 82 | } 83 | 84 | /** 85 | * @return int 86 | */ 87 | public function getLength() { 88 | return $this->length; 89 | } 90 | 91 | /** 92 | * @return float|int 93 | */ 94 | public function getBitRate() { 95 | return array_sum(array_keys($this->bitrates)) / count($this->bitrates); 96 | } 97 | 98 | /** 99 | * @return int 100 | */ 101 | public function getSampleRate() { 102 | return 8000; 103 | } 104 | 105 | /** 106 | * @return int 107 | */ 108 | public function getChannels() { 109 | return 1; 110 | } 111 | 112 | /** 113 | * @return bool 114 | */ 115 | public function isVariableBitRate() { 116 | return true; 117 | } 118 | 119 | /** 120 | * @return bool 121 | */ 122 | public function isLossless() { 123 | return false; 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/Adapters/Audio/FlacAdapter.php: -------------------------------------------------------------------------------- 1 | filename = $filename; 23 | $this->flac = new Flac($filename); 24 | } 25 | 26 | /** 27 | * @return int|null 28 | */ 29 | public function getLength() { 30 | return $this->flac->streamDuration; 31 | } 32 | 33 | /** 34 | * @return float|int 35 | */ 36 | public function getBitRate() { 37 | return floor($this->flac->streamBitsPerSample * $this->flac->streamTotalSamples / $this->flac->streamDuration); 38 | } 39 | 40 | /** 41 | * @return int|null 42 | */ 43 | public function getSampleRate() { 44 | return $this->flac->streamSampleRate; 45 | } 46 | 47 | /** 48 | * @return int|null 49 | */ 50 | public function getChannels() { 51 | return $this->flac->streamChannels; 52 | } 53 | 54 | /** 55 | * @return bool 56 | */ 57 | public function isVariableBitRate() { 58 | return true; 59 | } 60 | 61 | /** 62 | * @return bool 63 | */ 64 | public function isLossless() { 65 | return false; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/Adapters/Audio/Mp3Adapter.php: -------------------------------------------------------------------------------- 1 | filename = $filename; 23 | $this->mp3 = new Mp3Info($filename); 24 | } 25 | 26 | /** 27 | * @return float|int 28 | */ 29 | public function getLength() { 30 | return $this->mp3->duration; 31 | } 32 | 33 | /** 34 | * @return int 35 | */ 36 | public function getBitRate() { 37 | return $this->mp3->bitRate; 38 | } 39 | 40 | /** 41 | * @return int 42 | */ 43 | public function getSampleRate() { 44 | return $this->mp3->sampleRate; 45 | } 46 | 47 | /** 48 | * @return int 49 | */ 50 | public function getChannels() { 51 | return $this->mp3->channel == 'mono' ? 1 : 2; 52 | } 53 | 54 | /** 55 | * @return bool 56 | */ 57 | public function isVariableBitRate() { 58 | return $this->mp3->isVbr; 59 | } 60 | 61 | /** 62 | * @return bool 63 | */ 64 | public function isLossless() { 65 | return false; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/Adapters/Audio/OggAdapter.php: -------------------------------------------------------------------------------- 1 | filename = $filename; 27 | $this->stream = new BinaryStream($filename); 28 | $this->stream->saveGroup('ogg_page', array( 29 | 's:tag' => 4, 30 | 's:_' => 1, 31 | 'i:type' => 8, 32 | 's:__' => 8, 33 | 'i:number' => 32, 34 | 'i:page_sequence' => 32, 35 | 's:crc' => 4, 36 | 'i:segments_count' => 8, 37 | )); 38 | $this->stream->saveGroup('vorbis_identification_header', array( 39 | 'i:type' => 8, 40 | 's:tag' => 6, 41 | 'i:version' => 32, 42 | 'i:channels' => 8, 43 | 'i:sample_rate' => 32, 44 | 'i:bitrate_maximum' => 32, 45 | 'i:bitrate_nominal' => 32, 46 | 'i:bitrate_minimum' => 32, 47 | 'blocksize_0' => 4, 48 | 'blocksize_1' => 4, 49 | 'framing' => 1, 50 | 'align' => 7, 51 | )); 52 | $this->stream->saveGroup('vorbis_comments_header', array( 53 | 'i:type' => 8, 54 | 's:tag' => 6, 55 | 'i:length' => 32, 56 | )); 57 | $this->stream->saveGroup('vorbis_setup_header', array( 58 | 'i:type' => 8, 59 | 's:tag' => 6, 60 | 'i:codebook_count' => 8, 61 | )); 62 | $this->scan(); 63 | } 64 | 65 | /** 66 | * 67 | */ 68 | protected function scan() { 69 | $identification = $this->stream->readGroup('ogg_page'); 70 | $identification['segments'] = $this->stream->readString($identification['segments_count']); 71 | $header = $this->stream->readGroup('vorbis_identification_header'); 72 | $this->header = $header; 73 | 74 | $comments = $this->stream->readGroup('ogg_page'); 75 | $comments['segments'] = $this->stream->readString($comments['segments_count']); 76 | $header = $this->stream->readGroup('vorbis_comments_header'); 77 | $header['vendor'] = $this->stream->readString($header['length']); 78 | $header['list_length'] = $this->stream->readInteger(32); 79 | for ($i = 0; $i < $header['list_length']; $i++) { 80 | $header['list'][$i]['length'] = $this->stream->readInteger(32); 81 | $header['list'][$i]['value'] = $this->stream->readString($header['list'][$i]['length']); 82 | } 83 | $this->stream->skip(1); // skip 1 byte (with framing bit) 84 | 85 | // if ($this->stream->compare(4, 'OggS')) { // setup header is in third Ogg-page 86 | // $setup = $this->stream->readGroup('ogg_page'); 87 | // $setup['segments'] = $this->stream->readString($setup['segments_count']); 88 | // } 89 | // $header = $this->stream->readGroup('vorbis_setup_header'); 90 | } 91 | 92 | /** 93 | * @return float|int 94 | */ 95 | public function getLength() { 96 | return filesize($this->filename) / $this->getBitRate() * 8; 97 | } 98 | 99 | /** 100 | * @return float|int 101 | */ 102 | public function getBitRate() { 103 | if ($this->header['bitrate_nominal'] > 0) 104 | return $this->header['bitrate_nominal']; 105 | else 106 | return ($this->header['bitrate_maximum'] + $this->header['bitrate_minimum']) / 2; 107 | } 108 | 109 | /** 110 | * @return int 111 | */ 112 | public function getSampleRate() { 113 | return $this->header['sample_rate']; 114 | } 115 | 116 | /** 117 | * @return int 118 | */ 119 | public function getChannels() { 120 | return $this->header['channels']; 121 | } 122 | 123 | /** 124 | * @return bool 125 | */ 126 | public function isVariableBitRate() { 127 | // if ($this->header['bitrate_maximum'] == $this->header['bitrate_nominal'] && $this->header['bitrate_nominal'] == $this->header['bitrate_minimum']) 128 | // return false; 129 | if ($this->header['bitrate_nominal'] == 0 && $this->header['bitrate_maximum'] > 0 && $this->header['bitrate_minimum'] > 0) 130 | return true; 131 | return false; 132 | } 133 | 134 | /** 135 | * @return bool 136 | */ 137 | public function isLossless() { 138 | return false; 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /src/Adapters/Audio/WavAdapter.php: -------------------------------------------------------------------------------- 1 | filename = $filename; 23 | $this->wav = new Wave(); 24 | $this->wav->setFilename($filename); 25 | $this->metadata = $this->wav->getMetadata(); 26 | } 27 | 28 | /** 29 | * @return \BoyHagemann\Wave\Chunk\Fmt 30 | */ 31 | public function getMetadata() { 32 | return $this->metadata; 33 | } 34 | 35 | /** 36 | * @return float|int 37 | */ 38 | public function getLength() { 39 | $bytesPerSecond = $this->metadata->getBytesPerSecond(); 40 | return (filesize($this->filename) - 44) / $bytesPerSecond; 41 | } 42 | 43 | /** 44 | * @return float|int 45 | */ 46 | public function getBitRate() { 47 | return floor($this->metadata->getBytesPerSecond() / 1000) * 1000; 48 | } 49 | 50 | /** 51 | * @return int 52 | */ 53 | public function getSampleRate() { 54 | return $this->metadata->getSampleRate(); 55 | } 56 | 57 | /** 58 | * @return int 59 | */ 60 | public function getChannels() { 61 | return $this->metadata->getChannels(); 62 | } 63 | 64 | /** 65 | * @return bool 66 | */ 67 | public function isVariableBitRate() { 68 | return false; 69 | } 70 | 71 | /** 72 | * @return bool 73 | */ 74 | public function isLossless() { 75 | return false; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/Adapters/Audio/WmaAdapter.php: -------------------------------------------------------------------------------- 1 | length = $this->properties['send_length']; 24 | if (defined('DEBUG') && DEBUG) var_dump($this->streams); 25 | foreach ($this->streams as $stream) { 26 | if ($stream['type'] == ContainerAdapter::AUDIO) { 27 | $this->bitRate = $stream['bit_rate']; 28 | $this->sampleRate = $stream['sample_rate']; 29 | $this->channels = $stream['channels']; 30 | break; 31 | } 32 | } 33 | } 34 | 35 | /** 36 | * @return int 37 | */ 38 | public function getLength() { 39 | return $this->length; 40 | } 41 | 42 | /** 43 | * @return int 44 | */ 45 | public function getBitRate() { 46 | return $this->bitRate; 47 | } 48 | 49 | /** 50 | * @return int 51 | */ 52 | public function getSampleRate() { 53 | return $this->sampleRate; 54 | } 55 | 56 | /** 57 | * @return int 58 | */ 59 | public function getChannels() { 60 | return $this->channels; 61 | } 62 | 63 | /** 64 | * @return bool 65 | */ 66 | public function isVariableBitRate() { 67 | return false; 68 | } 69 | 70 | /** 71 | * @return bool 72 | */ 73 | public function isLossless() { 74 | return false; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/Adapters/AudioAdapter.php: -------------------------------------------------------------------------------- 1 | '3026b2758e66cf11a6d900aa0062ce6c', 63 | self::FILE_PROPERTIES => 'a1dcab8c47a9cf118ee400c00c205365', 64 | self::STREAM_PROPERTIES => '9107dcb7b7a9cf118ee600c00c205365', 65 | self::HEADER_EXTENSION => 'b503bf5f2ea9cf118ee300c00c205365', 66 | self::CODEC_LIST => '4052d1861d31d011a3a400a0c90348f6', 67 | self::SCRIPT_COMMAND => '301afb1e620bd011a39b00a0c90348f6', 68 | self::MARKER => '01cd87f451a9cf118ee600c00c205365', 69 | self::BITRATE_MUTUAL_EXCLUSION => 'dc29e2d6da35d111903400a0c90349be', 70 | self::ERROR_CORRECTION => '3526b2758e66cf11a6d900aa0062ce6c', 71 | self::CONTENT_DESCRIPTION => '3326b2758e66cf11a6d900aa0062ce6c', 72 | self::EXTENDED_CONTENT_DESCRIPTION => '40a4d0d207e3d21197f000a0c95ea850', 73 | self::CONTENT_BRANDING => 'fab3112223bdd211b4b700a0c955fc6e', 74 | self::STREAM_BITRATE_PROPERTIES => 'ce75f87b8d46d1118d82006097c9a2b2', 75 | self::CONTENT_ENCRYPTION => 'fbb3112223bdd211b4b700a0c955fc6e', 76 | self::EXTENDED_CONTENT_ENCRYPTION => '14e68a292226174cb935dae07ee9289c', 77 | self::DIGITAL_SIGNATURE => 'fcb3112223bdd211b4b700a0c955fc6e', 78 | self::PADDING => '74d40618dfca0945a4ba9aabcb96aae8', 79 | self::DATA => '3626b2758e66cf11a6d900aa0062ce6c', 80 | ); 81 | 82 | static protected $extension_uuids = array( 83 | self::EXTENDED_STREAM_PROPERTIES => 'cba5e61472c632438399a96952065b5a', 84 | self::ADVANCED_MUTUAL_EXCLUSION => 'cf4986a0754770468a166e35357566cd', 85 | self::GROUP_MUTUAL_EXCLUSION => '405a46d1795a3843b71be36b8fd6c249', 86 | self::STREAM_PRIORITIZATION => '5bd1fed4d3884f4581f0ed5c45999e24', 87 | self::BANDWIDTH_SHARING => 'e60996a67b51d211b6af00c04fd908e9', 88 | self::LANGUAGE_LIST => 'a946437ce0effc4bb229393ede415c85', 89 | self::METADATA => 'eacbf8c5af5b77488467aa8c44fa4cca', 90 | self::METADATA_LIBRARY => '941c23449894d149a1411d134e457054', 91 | self::INDEX_PARAMETERS => 'df29e2d6da35d111903400a0c90349be', 92 | self::MEDIA_OBJECT_INDEX_PARAMETERS => 'ad3b206b113fe448aca8d7613de2cfa7', 93 | self::TIMECODE_INDEX_PARAMETERS => '6d495ef597975d4b8c8b604dfe9bfb24', 94 | self::COMPATIBILITY => '3026b2758e66cf11a6d900aa0062ce6c', 95 | self::ADVANCED_CONTENT_ENCRYPTION => '338505438169e6499b74ad12cb86d58c', 96 | ); 97 | 98 | static protected $stream_type_uuids = array( 99 | self::AUDIO_MEDIA => '409e69f84d5bcf11a8fd00805f5c442b', 100 | self::VIDEO_MEDIA => 'c0ef19bc4d5bcf11a8fd00805f5c442b', 101 | self::COMMAND_MEDIA => 'c0cfda59e659d011a3ac00a0c90348f6', 102 | self::JFIF_MEDIA => '00e11bb64e5bcf11a8fd00805f5c442b', 103 | self::DEGRADABLE_JPEG_MEDIA => 'e07d903515e4cf11a91700805f5c442b', 104 | self::FILE_TRANSFER_MEDIA => '2c22bd911cf27a498b6d5aa86bfc0185', 105 | self::BINARY_MEDIA => 'e265fb3aef47f240ac2c70a90d71d343', 106 | ); 107 | 108 | public function __construct($filename) { 109 | if (!file_exists($filename) || !is_readable($filename)) throw new FileAccessException('File "'.$filename.'" is not available for reading!'); 110 | $this->filename = $filename; 111 | $this->stream = new BinaryStream($filename); 112 | $this->stream->saveGroup('object', array( 113 | 's:guid' => 16, 114 | 'i:size' => 64, 115 | )); 116 | $this->stream->saveGroup('header_object', array( 117 | 's:guid' => 16, 118 | 'i:size' => 64, 119 | 'i:count' => 32, 120 | 's:_' => 2, 121 | )); 122 | $this->stream->saveGroup('file_properties_object', array( 123 | 's:guid' => 16, 124 | 'i:size' => 64, 125 | 's:file_id' => 16, 126 | 'i:file_size' => 64, 127 | 'i:creation_date' => 64, 128 | 'i:data_packets_count' => 64, 129 | 'i:length' => 64, 130 | 'i:send_length' => 64, 131 | 'i:preroll' => 64, 132 | 'broadcast' => 1, 133 | 'seekable' => 1, 134 | '_' => 30, 135 | 'i:min_packet_size' => 32, 136 | 'i:max_packet_size' => 32, 137 | 'i:max_bit_rate' => 32, 138 | )); 139 | $this->stream->saveGroup('stream_properties_object', array( 140 | 's:guid' => 16, 141 | 'i:size' => 64, 142 | 's:type' => 16, 143 | 's:error_correction_type' => 16, 144 | 'i:time_offset' => 64, 145 | 'i:type_specific_data_length' => 32, 146 | 'i:error_correction_data_length' => 32, 147 | 'stream_number' => 7, 148 | '_' => 8, 149 | 'encrypted' => 1, 150 | 'i:_' => 32, 151 | )); 152 | $this->stream->saveGroup('stream_bitrate_properties_object', array( 153 | 's:guid' => 16, 154 | 'i:size' => 64, 155 | 'i:count' => 16, 156 | )); 157 | $this->stream->saveGroup('bitrate_record', array( 158 | 'stream_number' => 7, 159 | '_' => 9, 160 | 'i:bitrate' => 32, 161 | )); 162 | $this->stream->saveGroup('video_media_object', array( 163 | 'i:width' => 32, 164 | 'i:height' => 32, 165 | 'c:reserved' => 1, 166 | 'i:data_size' => 16, 167 | )); 168 | $this->stream->saveGroup('BITMAPINFOHEADER', array( 169 | 'i:format_data_size' => 32, 170 | 'i:image_width' => 32, 171 | 'i:image_height' => 32, 172 | 'i:reserved2' => 16, 173 | 'i:bits_per_pixel' => 16, 174 | 's:compression_id' => 4, 175 | 'i:image_size' => 32, 176 | 'i:horizontal_pixels_per_meter' => 32, 177 | 'i:vertical_pixels_per_meter' => 32, 178 | 'i:colors_used_count' => 32, 179 | 'i:important_colors_count' => 32, 180 | )); 181 | $this->stream->saveGroup('WAVEFORMATEX', array( 182 | 'i:codec' => 16, 183 | 'i:channels_count' => 16, 184 | 'i:sample_rate' => 32, 185 | 'i:byte_rate' => 32, 186 | 'i:alignment' => 16, 187 | 'i:bits_per_sample' => 16, 188 | 'i:codec_data_size' => 16, 189 | )); 190 | $this->stream->saveGroup('header_extension_object', array( 191 | 's:guid' => 16, 192 | 'i:size' => 64, 193 | 's:guid2' => 16, 194 | 'i:reserved' => 16, 195 | 'i:extension_size' => 32, 196 | )); 197 | $this->stream->saveGroup('extended_stream_properties_object', array( 198 | 's:guid' => 16, 199 | 'i:size' => 64, 200 | 'i:start_time' => 64, 201 | 'i:end_time' => 64, 202 | 'i:data_bitrate' => 32, 203 | 'i:buffer_size' => 32, 204 | 'i:initial_buffer_fullness' => 32, 205 | 'i:alternate_data_bitrate' => 32, 206 | 'i:alternate_buffer_size' => 32, 207 | 'i:alternate_initial_buffer_fullness' => 32, 208 | 'i:maximum_object_size' => 32, 209 | 'i:flags' => 32, 210 | 'i:stream_number' => 16, 211 | 'i:lang_id' => 16, 212 | 'i:avg_time_per_frame' => 64, 213 | 'i:names_count' => 16, 214 | 'i:payext_count' => 16, 215 | )); 216 | $this->stream->saveGroup('stream_name', array( 217 | 'i:id' => 16, 218 | 'i:length' => 16, 219 | )); 220 | $this->stream->saveGroup('payload_extension', array( 221 | 's:guid' => 16, 222 | 'i:size' => 16, 223 | 'i:length' => 32, 224 | )); 225 | $this->scan(); 226 | } 227 | 228 | protected function scan() { 229 | if (!$this->stream->compare(16, array(0x30, 0x26, 0xB2, 0x75, 0x8E, 0x66, 0xCF, 0x11, 0xA6, 0xD9, 0x00, 0xAA, 0x00, 0x62, 0xCE, 0x6C))) 230 | throw new ParsingException('This file is not an ASF file!'); 231 | 232 | $header = $this->stream->readGroup('header_object'); 233 | 234 | while (true) { 235 | $this->stream->mark('current_object'); 236 | $object_uuid = $this->stringToUUid($this->stream->readString(16)); 237 | $this->stream->go('current_object'); 238 | 239 | 240 | if (!in_array($object_uuid, self::$uuids)) { 241 | $object = $this->stream->readGroup('object'); 242 | $this->stream->skip($object['size'] - 24); 243 | break; 244 | } 245 | 246 | $object_type = array_search($object_uuid, self::$uuids); 247 | if (defined('DEBUG') && DEBUG) var_dump($object_type); 248 | switch ($object_type) { 249 | case self::FILE_PROPERTIES: 250 | $file_properties = $this->stream->readGroup('file_properties_object'); 251 | $file_properties['send_length'] = $file_properties['send_length'] / 10000000; 252 | $this->properties = $file_properties; 253 | break; 254 | case self::STREAM_BITRATE_PROPERTIES: 255 | $stream_bitrate_properties = $this->stream->readGroup('stream_bitrate_properties_object'); 256 | $bitrates = array(); 257 | for ($i = 0; $i < $stream_bitrate_properties['count']; $i++) { 258 | $bitrate = $this->stream->readGroup('bitrate_record'); 259 | $bitrates[$bitrate['stream_number']] = $bitrate['bitrate']; 260 | } 261 | $this->streams_bitrates = $bitrates; 262 | break; 263 | case self::STREAM_PROPERTIES: 264 | $stream_properties = $this->stream->readGroup('stream_properties_object'); 265 | $stream_properties['type'] = $this->stringToUUid($stream_properties['type']); 266 | 267 | if (!in_array($stream_properties['type'], self::$stream_type_uuids)) { 268 | $this->stream->skip($stream_properties['type_specific_data_length']); 269 | $this->stream->skip($stream_properties['error_correction_data_length']); 270 | continue; 271 | } 272 | 273 | switch (array_search($stream_properties['type'], self::$stream_type_uuids)) { 274 | case self::VIDEO_MEDIA: 275 | $stream_properties += $this->stream->readGroup('video_media_object'); 276 | $stream_properties += $this->stream->readGroup('BITMAPINFOHEADER'); 277 | $this->stream->skip($stream_properties['format_data_size'] - 40); // 40 - size of BITMAPINFOHEADER structure 278 | $this->streams[$stream_properties['stream_number']] = array( 279 | 'type' => ContainerAdapter::VIDEO, 280 | 'codec' => $stream_properties['compression_id'], 281 | 'length' => 0, 282 | 'framerate' => 0, 283 | 'width' => $stream_properties['width'], 284 | 'height' => $stream_properties['height'], 285 | ); 286 | break; 287 | 288 | case self::AUDIO_MEDIA: 289 | $stream_properties += $this->stream->readGroup('WAVEFORMATEX'); 290 | $this->stream->skip($stream_properties['codec_data_size']); 291 | $this->streams[$stream_properties['stream_number']] = array( 292 | 'type' => ContainerAdapter::AUDIO, 293 | 'codec' => null, 294 | 'length' => 0, 295 | 'channels' => $stream_properties['channels_count'], 296 | 'sample_rate' => $stream_properties['sample_rate'], 297 | 'bits_per_sample' => $stream_properties['bits_per_sample'], 298 | 'bit_rate' => $stream_properties['byte_rate'] * 8, 299 | ); 300 | break; 301 | 302 | default: 303 | // another media type, just skip it 304 | $this->stream->skip($stream_properties['type_specific_data_length']); 305 | break; 306 | } 307 | $this->stream->skip($stream_properties['error_correction_data_length']); 308 | // var_dump($stream_properties); 309 | break; 310 | case self::HEADER_EXTENSION: 311 | $header_extension = $this->stream->readGroup('header_extension_object'); 312 | 313 | $extended_stream_properties_count = 0; 314 | 315 | while (true) { 316 | $this->stream->mark('current_extension_object'); 317 | $extension_object_uuid = $this->stringToUUid($this->stream->readString(16)); 318 | $this->stream->go('current_extension_object'); 319 | // skip unknown extension object 320 | if (!in_array($extension_object_uuid, self::$extension_uuids)) { 321 | $extension_object = $this->stream->readGroup('object'); 322 | $this->stream->skip($extension_object['size'] - 24); 323 | break; 324 | } 325 | 326 | $extension_object_type = array_search($extension_object_uuid, self::$extension_uuids); 327 | switch ($extension_object_type) { 328 | case self::EXTENDED_STREAM_PROPERTIES: 329 | $extended_stream_properties = $this->stream->readGroup('extended_stream_properties_object'); 330 | // var_dump($extended_stream_properties); 331 | $this->stream->go('current_extension_object'); 332 | $this->stream->skip($extended_stream_properties['size']); // 82 - size of extended stream properties structure 333 | 334 | $this->streams[$extended_stream_properties['stream_number'] - 1]['length'] = ($extended_stream_properties['end_time'] - $extended_stream_properties['start_time']); 335 | 336 | // jump out when all stream extended properties retrieved 337 | if (++$extended_stream_properties_count >= count($this->streams)) break(2); 338 | continue; 339 | 340 | // skipping these objects to arrive extended stream properties 341 | case self::ADVANCED_MUTUAL_EXCLUSION: 342 | case self::GROUP_MUTUAL_EXCLUSION: 343 | case self::STREAM_PRIORITIZATION: 344 | case self::BANDWIDTH_SHARING: 345 | case self::LANGUAGE_LIST: 346 | case self::METADATA: 347 | case self::METADATA_LIBRARY: 348 | case self::INDEX_PARAMETERS: 349 | case self::MEDIA_OBJECT_INDEX_PARAMETERS: 350 | case self::TIMECODE_INDEX_PARAMETERS: 351 | case self::COMPATIBILITY: 352 | case self::ADVANCED_CONTENT_ENCRYPTION: 353 | $extension_object = $this->stream->readGroup('object'); 354 | $this->stream->skip($extension_object['size'] - 24); // 24 - size of "object" structure 355 | continue; 356 | 357 | // another object, jump out of header extension handling 358 | default: 359 | break(2); 360 | 361 | } 362 | } 363 | 364 | break; 365 | 366 | case self::HEADER: 367 | case self::FILE_PROPERTIES: 368 | case self::STREAM_PROPERTIES: 369 | case self::HEADER_EXTENSION: 370 | case self::CODEC_LIST: 371 | case self::SCRIPT_COMMAND: 372 | case self::MARKER: 373 | case self::BITRATE_MUTUAL_EXCLUSION: 374 | case self::ERROR_CORRECTION: 375 | case self::CONTENT_DESCRIPTION: 376 | case self::EXTENDED_CONTENT_DESCRIPTION: 377 | case self::CONTENT_BRANDING: 378 | case self::STREAM_BITRATE_PROPERTIES: 379 | case self::CONTENT_ENCRYPTION: 380 | case self::EXTENDED_CONTENT_ENCRYPTION: 381 | case self::DIGITAL_SIGNATURE: 382 | case self::PADDING: 383 | $object = $this->stream->readGroup('object'); 384 | $this->stream->skip($object['size'] - 24); 385 | continue; 386 | 387 | default: 388 | if (defined('DEBUG') && DEBUG) var_dump($object_type); 389 | break(2); 390 | } 391 | 392 | // if (!in_array($object_type, array(self::FILE_PROPERTIES, self::STREAM_BITRATE_PROPERTIES, self::STREAM_PROPERTIES, self::HEADER_EXTENSION))) break; 393 | } 394 | } 395 | 396 | protected function stringToUUid($object_uuid_string) { 397 | $object_uuid = null; 398 | for ($i = 0; $i < 16; $i++) 399 | $object_uuid .= str_pad(dechex(ord($object_uuid_string[$i])), 2, '0', STR_PAD_LEFT); 400 | return $object_uuid; 401 | } 402 | 403 | public function countStreams() { 404 | return count($this->streams); 405 | } 406 | 407 | public function countVideoStreams() { 408 | $count = 0; 409 | foreach ($this->streams as $stream) 410 | if ($stream['type'] == ContainerAdapter::VIDEO) $count++; 411 | return $count; 412 | } 413 | 414 | public function countAudioStreams() { 415 | $count = 0; 416 | foreach ($this->streams as $stream) 417 | if ($stream['type'] == ContainerAdapter::AUDIO) $count++; 418 | return $count; 419 | } 420 | 421 | public function getStreams() { 422 | return $this->streams; 423 | } 424 | } 425 | -------------------------------------------------------------------------------- /src/Adapters/Containers/MatroskaContainer.php: -------------------------------------------------------------------------------- 1 | filename = $filename; 179 | $this->stream = new BinaryStream($filename); 180 | $this->stream->setEndian(BinaryStream::BIG); 181 | $this->scan(); 182 | } 183 | 184 | protected function scan() { 185 | if (!$this->stream->compare(4, array(0x1A, 0x45, 0xDF, 0xA3))) 186 | throw new ParsingException('This file is not a Matroska container!'); 187 | $this->stream->skip(4); 188 | // master size 189 | $size = $this->readEbmlElementSize($bytesForSize); 190 | $this->stream->markOffset(4 + $bytesForSize + $size, 'data'); 191 | 192 | // read element size 193 | $this->ensureEbmlElementId(array(0x42, 0x86)); 194 | $size = $this->readEbmlElementSize(); 195 | $this->header['version'] = $this->stream->readInteger($size * 8); 196 | 197 | $this->ensureEbmlElementId(array(0x42, 0xF7)); 198 | $size = $this->readEbmlElementSize(); 199 | $this->header['minVersion'] = $this->stream->readInteger($size * 8); 200 | 201 | $this->ensureEbmlElementId(array(0x42, 0xF2)); 202 | $size = $this->readEbmlElementSize(); 203 | $this->header['maxIdLength'] = $this->stream->readInteger($size * 8); 204 | 205 | $this->ensureEbmlElementId(array(0x42, 0xF3)); 206 | $size = $this->readEbmlElementSize(); 207 | $this->header['maxSizeLength'] = $this->stream->readInteger($size * 8); 208 | 209 | $this->ensureEbmlElementId(array(0x42, 0x82)); 210 | $size = $this->readEbmlElementSize(); 211 | $this->header['docType'] = $this->stream->readString($size); 212 | 213 | $this->ensureEbmlElementId(array(0x42, 0x87)); 214 | $size = $this->readEbmlElementSize(); 215 | $this->header['docTypeVersion'] = $this->stream->readInteger($size * 8); 216 | 217 | $this->ensureEbmlElementId(array(0x42, 0x85)); 218 | $size = $this->readEbmlElementSize(); 219 | $this->header['docTypeMinVersion'] = $this->stream->readInteger($size * 8); 220 | 221 | // go to data 222 | $this->stream->go('data'); 223 | // check for segment 224 | $this->ensureEbmlElementId(array(0x18, 0x53, 0x80, 0x67)); 225 | $size = $this->readEbmlElementSize($b); 226 | // var_dump($b); 227 | 228 | $segments = array(); 229 | $i = 1; 230 | while (!$this->stream->isEnd()) { 231 | if ($i++ % 1000 == 0) break; //var_dump(dechex($id), memory_get_usage(), ftell($this->stream->fp)); 232 | $pos = ftell($this->stream->fp); 233 | $id = $this->readEbmlElementId(); 234 | $size = $this->readEbmlElementSize(); 235 | // var_dump(dechex($id), $size, $before_size_pos, ftell($this->stream->fp)); 236 | // $segments[dechex($id)]; 237 | // switch ($id) { 238 | // case self::SEGMENT_INFO_ID: 239 | // var_dump($size); 240 | // break; 241 | 242 | // default: 243 | // var_dump(dechex($id), $size); 244 | // $this->stream->skip($size); 245 | // break; 246 | // } 247 | $segment_i = 0; 248 | // var_dump(dechex($id)); 249 | switch ($id) { 250 | case self::SEGMENT_INFO_ID: 251 | // find for duration 252 | while (!$this->stream->isEnd()) { 253 | $this->stream->mark('next_id'); 254 | $inner_id = $this->readEbmlElementId($bytesForId); 255 | 256 | switch ($inner_id) { 257 | case self::INFO_SEGMENT_UID: 258 | case self::INFO_SEGMENT_FILENAME: 259 | case self::INFO_PREV_UID: 260 | case self::INFO_PREV_FILENAME: 261 | case self::INFO_NEXT_UID: 262 | case self::INFO_NEXT_FILENAME: 263 | case self::INFO_SEGMENT_FAMILY: 264 | case self::INFO_CHAPTER_TRANSLATE: 265 | case self::INFO_CHAPTER_TRANSLATEEDITIONUID: 266 | case self::INFO_CHAPTER_TRANSLATECODEC: 267 | case self::INFO_CHAPTER_TRANSLATEID: 268 | case self::INFO_DATE_UTC: 269 | case self::INFO_TITLE: 270 | case self::INFO_MUXING_APP: 271 | case self::INFO_WRITING_APP: 272 | case self::INFO_TITLE: // here is title! 273 | $this->stream->skip($this->readEbmlElementSize($bytesForSize)); 274 | break; 275 | 276 | case self::INFO_TIMECODE_SCALE: 277 | // $data = $this->stream->readBits(array('scale' => $inner_size * 8)); 278 | // $timecode_scale = $data['scale']; 279 | $timecode_scale = $this->stream->readInteger($this->readEbmlElementSize($bytesForSize) * 8); 280 | $timecode_scale = 1000000000 / $timecode_scale; 281 | break; 282 | 283 | case self::INFO_DURATION: 284 | $duration = $this->stream->readFloat($this->readEbmlElementSize($bytesForSize) * 8); 285 | break; 286 | 287 | default: 288 | // var_dump(dechex($inner_id)); 289 | $this->stream->go('next_id'); 290 | break(2); 291 | } 292 | } 293 | if (isset($duration)) { 294 | $this->duration = isset($timecode_scale) ? $duration / $timecode_scale : $duration / 1000; 295 | } 296 | $segment_i++; 297 | break; 298 | 299 | case self::TRACKS_ID: 300 | $track_id = 0; 301 | // var_dump('Tracks block. size: '.$size.', bitOffset: '.$this->stream->bitOffset.' offset: '.$this->stream->offset, $bytesForSize, ftell($this->stream->fp)); 302 | while (!$this->stream->isEnd()) { 303 | $this->stream->mark('next_id'); 304 | $inner_id = $this->readEbmlElementId($bytesForId); 305 | $inner_size = $this->readEbmlElementSize($bytesForSize); 306 | // var_dump(dechex($id), $size, dechex($inner_id), $inner_size, ftell($this->stream->fp)); 307 | $track_i = 0; 308 | if ($inner_id != self::TRACK_ENTRY_ID) { 309 | $this->stream->go('next_id'); 310 | break(1); 311 | } 312 | 313 | while (!$this->stream->isEnd()) { 314 | $this->stream->mark('next_id'); 315 | $inner_id = $this->readEbmlElementId($bytesForId); 316 | $inner_size = $this->readEbmlElementSize($bytesForSize); 317 | // var_dump(dechex($inner_id), $inner_size); 318 | switch ($inner_id) { 319 | case self::TRACK_UID_ID: 320 | case self::FLAG_ENABLED_ID: 321 | case self::FLAG_DEFAULT_ID: 322 | case self::FLAG_FORCED_ID: 323 | case self::FLAG_LACING_ID: 324 | case self::MIN_CACHE_ID: 325 | case self::MAX_CACHE_ID: 326 | case self::DEFAULT_DURATION_ID: 327 | case self::DEFAULT_DECODED_FIELD_DURATION_ID: 328 | case self::TRACK_TIMECODE_SCALE_ID: 329 | case self::TRACK_OFFSET_ID: 330 | case self::MAX_BLOCK_ADDITION_ID_ID: 331 | case self::NAME_ID: 332 | case self::LANGUAGE_ID: 333 | case self::CODEC_ID_ID: 334 | case self::CODEC_PRIVATE_ID: 335 | case self::CODEC_NAME_ID: 336 | case self::ATTACHMENT_LINK_ID: 337 | case self::CODEC_SETTINGS_ID: 338 | case self::CODEC_INFO_URL_ID: 339 | case self::CODEC_DOWNLOAD_URL_ID: 340 | case self::CODEC_DECODE_ALL_ID: 341 | case self::TRACK_OVERLAY_ID: 342 | case self::CODEC_DELAY_ID: 343 | case self::SEEK_PRE_ROLL_ID: 344 | case self::TRACK_TRANSLATE_ID: 345 | case self::TRACK_OPERATION_ID: 346 | case self::TRICK_TRACK_SEGMENT_UI_D_ID: 347 | case self::TRICK_TRACK_FLAG_ID: 348 | case self::TRICK_MASTER_TRACK_UI_D_ID: 349 | case self::TRICK_MASTER_TRACK_SEGMENT_UI_D_ID: 350 | case self::CONTENT_ENCODINGS_ID: 351 | $this->stream->skip($inner_size); 352 | break; 353 | 354 | case self::TRACK_TYPE_ID: 355 | $type = $this->stream->readInteger($inner_size * 8); 356 | switch ($type) { 357 | case 1: 358 | $this->streams[$track_i]['type'] = 'video'; 359 | $this->streams[$track_i]['framerate'] = 0; 360 | break; 361 | case 2: 362 | $this->streams[$track_i]['type'] = 'audio'; 363 | break; 364 | } 365 | break; 366 | 367 | case self::VIDEO_ID: 368 | while (!$this->stream->isEnd()) { 369 | $this->stream->mark('next_id'); 370 | $inner_id = $this->readEbmlElementId($bytesForId); 371 | $inner_size = $this->readEbmlElementSize($bytesForSize); 372 | // var_dump(dechex($inner_id)); 373 | switch ($inner_id) { 374 | case self::FLAG_INTERLACED: 375 | case self::FIELD_ORDER: 376 | case self::STEREO_MODE: 377 | case self::ALPHA_MODE: 378 | case self::OLD_STEREO_MODE: 379 | case self::PIXEL_CROP_BOTTOM: 380 | case self::PIXEL_CROP_TOP: 381 | case self::PIXEL_CROP_LEFT: 382 | case self::PIXEL_CROP_RIGHT: 383 | case self::DISPLAY_WIDTH: 384 | case self::DISPLAY_HEIGHT: 385 | case self::DISPLAY_UNIT: 386 | case self::ASPECT_RATIO_TYPE: 387 | case self::COLOUR_SPACE: 388 | case self::GAMMA_VALUE: 389 | case self::COLOUR: 390 | $this->stream->skip($inner_size); 391 | break; 392 | 393 | case self::PIXEL_WIDTH: 394 | $this->streams[$track_i]['width'] = $this->stream->readInteger($inner_size * 8); 395 | break; 396 | 397 | case self::PIXEL_HEIGHT: 398 | $this->streams[$track_i]['height'] = $this->stream->readInteger($inner_size * 8); 399 | break; 400 | 401 | case self::FRAME_RATE: 402 | $this->streams[$track_i]['framerate'] = $this->stream->readFloat($inner_size * 8); 403 | break; 404 | 405 | default: 406 | $this->stream->go('next_id'); 407 | break(2); 408 | } 409 | } 410 | break; 411 | 412 | case self::AUDIO_ID: 413 | while (!$this->stream->isEnd()) { 414 | $this->stream->mark('next_id'); 415 | $inner_id = $this->readEbmlElementId($bytesForId); 416 | $inner_size = $this->readEbmlElementSize($bytesForSize); 417 | // var_dump(dechex($inner_id)); 418 | switch ($inner_id) { 419 | case self::SAMPLING_FREQUENCY: 420 | case self::CHANNEL_POSITIONS: 421 | $this->stream->skip($inner_size); 422 | break; 423 | 424 | case self::CHANNELS: 425 | $this->streams[$track_i]['channels'] = $this->stream->readInteger($inner_size * 8); 426 | break; 427 | 428 | case self::PIXEL_WIDTH: 429 | $this->streams[$track_i]['sample_rate'] = $this->stream->readInteger($inner_size * 8); 430 | break; 431 | 432 | case self::BIT_DEPTH: 433 | $this->streams[$track_i]['bit_rate'] = $this->stream->readInteger($inner_size * 8); 434 | break; 435 | 436 | default: 437 | $this->stream->go('next_id'); 438 | break(2); 439 | } 440 | } 441 | break; 442 | 443 | case self::TRACK_NUMBER_ID: 444 | $track_id = $this->stream->readInteger($inner_size * 8); 445 | break; 446 | 447 | default: 448 | // var_dump(dechex($inner_id)); 449 | $this->stream->go('next_id'); 450 | break(2); 451 | } 452 | } 453 | $track_i++; 454 | } 455 | return true; 456 | break; 457 | 458 | default: 459 | $this->stream->skip($size); 460 | break; 461 | } 462 | // var_dump(dechex($id), $size, ftell($this->stream->fp)); 463 | if ($i == 5) break; 464 | } 465 | // var_dump($segments); 466 | } 467 | 468 | protected function ensureEbmlElementId($bytes) { 469 | if (!$this->stream->compare(count($bytes), $bytes)) { 470 | throw new ParsingException('File should contain element "'.implode('-', array_map('dechex', $bytes)).'" at this offset!'); 471 | } 472 | $this->stream->skip(count($bytes)); 473 | } 474 | 475 | protected function readEbmlElementId(&$bytesForId = 0) { 476 | $id = ord($this->stream->readChar()); 477 | if ($id & 0x80) $bytesForId = 1; 478 | else if ($id & 0x40) $bytesForId = 2; 479 | else if ($id & 0x20) $bytesForId = 3; 480 | else if ($id & 0x10) $bytesForId = 4; 481 | $i = $bytesForId - 1; 482 | while ($i-- > 0) 483 | $id = ($id << 8) + ord($this->stream->readChar()); 484 | return $id; 485 | } 486 | 487 | protected function readEbmlElementSize(&$bytesForSize = 0, $debuf = false) { 488 | // $this->stream->readString(1); 489 | // $this->stream->skip(-1); 490 | $bytesForSize = 0; 491 | $bit = false; 492 | while ($bit === false) { 493 | $bit = $this->stream->readBit(); 494 | $bytesForSize++; 495 | } 496 | // read data until the end of byte 497 | $end = 8 * $bytesForSize; 498 | // var_dump($bytesForSize, $end); 499 | $size = 0; 500 | $i = $bytesForSize; 501 | while ($i++ < $end) { 502 | $bit = $this->stream->readBit(); 503 | $size = ($size << 1) + $bit; 504 | } 505 | // read 506 | return $size; 507 | } 508 | } 509 | -------------------------------------------------------------------------------- /src/Adapters/Containers/Mpeg4Part12Adapter.php: -------------------------------------------------------------------------------- 1 | filename = $filename; 48 | $this->stream = new BinaryStream($filename); 49 | $this->stream->setEndian(BinaryStream::BIG); 50 | $this->stream->saveGroup('box_header', array( 51 | 'i:size' => 32, 52 | 's:type' => 4, 53 | )); 54 | $this->stream->saveGroup('full_box_header', array( 55 | 'i:size' => 32, 56 | 's:type' => 4, 57 | 'i:version' => 8, 58 | 'flags' => 24, 59 | )); 60 | $this->stream->saveGroup('ftyp_box', array( 61 | 'i:size' => 32, 62 | 's:type' => 4, 63 | 's:major' => 4, 64 | 's:minor' => 4, 65 | )); 66 | $this->stream->saveGroup('mvhd_box_big', array( 67 | 'i:creation_time' => 64, 68 | 'i:modification_time' => 64, 69 | 'i:timescale' => 32, 70 | 'i:duration' => 64, 71 | 'i:rate' => 32, 72 | 'i:volume' => 16, 73 | 's:_' => 70, 74 | 'i:next_track' => 32, 75 | )); 76 | $this->stream->saveGroup('mvhd_box_little', array( 77 | 'i:creation_time' => 32, 78 | 'i:modification_time' => 32, 79 | 'i:timescale' => 32, 80 | 'i:duration' => 32, 81 | 'i:rate' => 32, 82 | 'i:volume' => 16, 83 | 's:_' => 70, 84 | 'i:next_track' => 32, 85 | )); 86 | $this->stream->saveGroup('tkhd_box_big', array( 87 | 'i:creation_time' => 64, 88 | 'i:modification_time' => 64, 89 | 'i:track_id' => 32, 90 | 'i:reserved' => 32, 91 | 'i:duration' => 64, 92 | 's:_' => 8, 93 | 'i:layer' => 16, 94 | 'i:group' => 16, 95 | 'i:volume' => 16, 96 | 's:__' => 38, 97 | 'i:width' => 32, 98 | 'i:height' => 32, 99 | )); 100 | $this->stream->saveGroup('tkhd_box_little', array( 101 | 'i:creation_time' => 32, 102 | 'i:modification_time' => 32, 103 | 'i:track_id' => 32, 104 | 'i:reserved' => 32, 105 | 'i:duration' => 32, 106 | 's:_' => 8, 107 | 'i:layer' => 16, 108 | 'i:group' => 16, 109 | 'i:volume' => 16, 110 | 's:__' => 38, 111 | 'i:width' => 32, 112 | 'i:height' => 32, 113 | )); 114 | $this->stream->saveGroup('mdhd_box_big', array( 115 | 'i:creation_time' => 64, 116 | 'i:modification_time' => 64, 117 | 'i:timescale' => 32, 118 | 'i:duration' => 64, 119 | 'i:language' => 16, 120 | 'i:_' => 16, 121 | )); 122 | $this->stream->saveGroup('mdhd_box_little', array( 123 | 'i:creation_time' => 32, 124 | 'i:modification_time' => 32, 125 | 'i:timescale' => 32, 126 | 'i:duration' => 32, 127 | 'i:language' => 16, 128 | 'i:_' => 16, 129 | )); 130 | $this->stream->saveGroup('hdlr_box', array( 131 | 'i:defined' => 32, 132 | 's:handler_type' => 4, 133 | 's:_' => 12, 134 | )); 135 | $this->stream->saveGroup('AudioSampleEntry', array( 136 | 's:_' => 6, 137 | 'i:reference' => 16, 138 | 's:__' => 8, 139 | 'i:channelCount' => 16, 140 | 'i:sampleSize' => 16, 141 | 'i:defined' => 16, 142 | 'i:_' => 16, 143 | 'i:sampleRate' => 32, 144 | )); 145 | $this->stream->saveGroup('VideoSampleEntry', array( 146 | 's:_' => 24, 147 | 'i:width' => 16, 148 | 'i:height' => 16, 149 | 'i:horizontal_resolution' => 32, 150 | 'i:vertical_resolution' => 32, 151 | 'i:_' => 32, 152 | 'i:frame_count' => 16, 153 | 's:compressor' => 32, 154 | 's:__' => 4, 155 | )); 156 | $this->scan(); 157 | } 158 | 159 | /** 160 | * @throws ParsingException 161 | */ 162 | protected function scan() { 163 | $ftyp = $this->stream->readGroup('ftyp_box'); 164 | if ($ftyp['type'] !== 'ftyp') 165 | throw new ParsingException('This file is not an MPEG-4 Part 12/14 container!'); 166 | 167 | switch ($ftyp['major']) { 168 | case 'isom': $this->type = self::ISO; break; 169 | case 'mp41': $this->type = self::MP4_1; break; 170 | case 'mp42': $this->type = self::MP4_2; break; 171 | } 172 | 173 | $this->stream->skip($ftyp['size'] - 16); 174 | 175 | if ($this->getNextBoxType() === 'mdat') { 176 | $this->stream->mark('mdat');; 177 | $this->mdat = $this->stream->readGroup('box_header'); 178 | $this->stream->skip($this->mdat['size'] - 8); // 8 - size of box header structure 179 | } 180 | 181 | while ($this->getNextBoxType() !== 'moov') { 182 | $useless_box = $this->stream->readGroup('box_header'); 183 | $this->stream->skip($useless_box['size'] - 8); 184 | } 185 | 186 | $this->stream->mark('moov'); 187 | $moov = $this->stream->readGroup('box_header'); 188 | 189 | if ($moov['type'] !== 'moov') 190 | throw new ParsingException('This file does not have "moov" box! First actual box is: '.json_encode($moov, JSON_PRETTY_PRINT)); 191 | 192 | $this->stream->mark('mvhd'); 193 | $this->mvhd = $this->stream->readGroup('full_box_header'); 194 | if ($this->mvhd['type'] !== 'mvhd') 195 | throw new ParsingException('This file does not have "mvhd" box!'); 196 | $this->mvhd += $this->stream->readGroup('mvhd_box_'.($this->mvhd['version'] == 0 ? 'little' : 'big')); 197 | 198 | $this->stream->go('mvhd'); 199 | $this->stream->skip($this->mvhd['size'] - 12); 200 | 201 | // var_dump($this->mvhd); 202 | $i = 0; 203 | // tracks scanning 204 | while ($this->getNextBoxType() === 'trak') { 205 | $this->stream->mark('track_'.$i); 206 | $trak = $this->stream->readGroup('box_header'); 207 | 208 | if ($this->getNextBoxType() !== 'tkhd') 209 | throw new ParsingException('This file does not have "tkhd" box!'); 210 | $tkhd = $this->stream->readGroup('full_box_header'); 211 | $tkhd += $this->stream->readGroup('tkhd_box_'.($tkhd['version'] == 0 ? 'little' : 'big')); 212 | $this->streams[$tkhd['track_id']] = array( 213 | 'length' => $tkhd['duration'] / $this->mvhd['timescale'], 214 | 'codec' => null 215 | ); 216 | 217 | // track headers scanning 218 | while (in_array($next_box = $this->getNextBoxType(), array('tref', 'edts', 'mdia'))) { 219 | switch ($next_box) { 220 | case 'tref': 221 | case 'edts': 222 | $box = $this->stream->readGroup('box_header'); 223 | $this->stream->skip($box['size'] - 8); // 8 - size of box header structure 224 | break; 225 | 226 | case 'mdia': 227 | $this->stream->mark('mdia'); 228 | $mdia = $this->stream->readGroup('box_header'); 229 | 230 | // media information scanning 231 | while (in_array($next_box = $this->getNextBoxType(), array('mdhd', 'hdlr', 'minf', 'vmhd', 'smhd', 'hmhd', 'nmhd', 'dinf', 'dref', 'stbl', 'stsd', 'stts', 'ctts', 'stsc', 'stsz', 'stz2', 'stco', 'co64', 'stss', 'stsh', 'padb', 'stdp', 'sdtp', 'sbgp', 'sgpd', 'subs'))) { 232 | switch ($next_box) { 233 | case 'mdhd': 234 | $mdhd = $this->stream->readGroup('full_box_header'); 235 | if ($mdhd['type'] !== 'mdhd') 236 | throw new ParsingException('This file does not have "mdhd" box!'); 237 | 238 | $mdhd += $this->stream->readGroup('mdhd_box_'.($mdhd['version'] == 0 ? 'little' : 'big')); 239 | break; 240 | 241 | case 'hdlr': 242 | $hdlr = $this->stream->readGroup('full_box_header'); 243 | $hdlr += $this->stream->readGroup('hdlr_box'); 244 | $this->streams[$tkhd['track_id']]['type'] = ($hdlr['handler_type'] === 'vide') ? ContainerAdapter::VIDEO 245 | : ($hdlr['handler_type'] === 'soun' ? ContainerAdapter::AUDIO : null); 246 | $this->stream->skip($hdlr['size'] - 32); // 32 - size of full_box_header + hdlr_box 247 | break; 248 | 249 | // should be here to make possible scanning of inner boxes 250 | case 'minf': 251 | $minf = $this->stream->readGroup('box_header'); 252 | break; 253 | 254 | // should be here to make possible scanning of inner boxes 255 | case 'stbl': 256 | $stbl = $this->stream->readGroup('box_header'); 257 | break; 258 | 259 | case 'stsd': 260 | $box = $this->stream->readGroup('full_box_header'); 261 | $box['entry_count'] = $this->stream->readInteger(32); 262 | if ($box['entry_count'] > 1) 263 | throw new ParsingException('It\' strange! File has more 1 stsd entries in one track! Please, send your file text of this error as Pull Request on github to discover the problem.'); 264 | 265 | $box = $this->stream->readGroup('box_header'); 266 | switch ($this->streams[$tkhd['track_id']]['type']) { 267 | case ContainerAdapter::AUDIO: 268 | $box += $this->stream->readGroup('AudioSampleEntry'); 269 | $this->streams[$tkhd['track_id']]['codec'] = $box['type']; 270 | $this->streams[$tkhd['track_id']]['channels'] = $box['channelCount']; 271 | $this->streams[$tkhd['track_id']]['sample_rate'] = $box['sampleRate'] >> 16; 272 | break; 273 | 274 | case ContainerAdapter::VIDEO: 275 | $box += $this->stream->readGroup('VideoSampleEntry'); 276 | $this->streams[$tkhd['track_id']]['codec'] = $box['type']; 277 | $this->streams[$tkhd['track_id']]['width'] = $box['width']; 278 | $this->streams[$tkhd['track_id']]['height'] = $box['height']; 279 | $this->streams[$tkhd['track_id']]['framerate'] = 0; 280 | break; 281 | } 282 | break; 283 | 284 | // skip any non-informative box 285 | default: 286 | $box = $this->stream->readGroup('box_header'); 287 | $this->stream->skip($box['size'] - 8); 288 | break; 289 | } 290 | } 291 | $this->stream->go('mdia'); 292 | $this->stream->skip($mdia['size']); 293 | break; 294 | } 295 | } 296 | 297 | // jump to next 2nd-level box 298 | $this->stream->go('track_'.$i++); 299 | $this->stream->skip($trak['size']); 300 | 301 | $i++; 302 | 303 | } 304 | 305 | if (empty($this->streams) && !$this->stream->isEnd()) { 306 | var_dump('NEXT BOX TYPE IS: '.$this->getNextBoxType()); 307 | } 308 | 309 | // find for mdat 310 | if (empty($this->mdat)) { 311 | while (!$this->stream->isEnd() && $this->getNextBoxType() !== 'mdat') { 312 | $box = $this->stream->readGroup('box_header'); 313 | $this->stream->skip($box['size'] - 8); // 8 - size of box_header structure 314 | } 315 | if (!$this->stream->isEnd()) $this->mdat = $this->stream->readGroup('box_header'); 316 | } 317 | 318 | } 319 | 320 | /** 321 | * @return bool|string 322 | */ 323 | protected function getNextBoxType() { 324 | $this->stream->mark('current_box'); 325 | $this->stream->skip(4); 326 | $type = $this->stream->readString(4); 327 | $this->stream->go('current_box'); 328 | return $type; 329 | } 330 | 331 | /** 332 | * @return bool|null|string 333 | */ 334 | protected function readUntilDoubleNull() { 335 | $string = null; 336 | $previous_null = false; 337 | while (!$this->stream->isEnd()) { 338 | $char = $this->stream->readChar(); 339 | if ($char === "\00") { 340 | if ($previous_null) { 341 | $string = substr($string, 0, -1); 342 | break; 343 | } 344 | else 345 | $previous_null = true; 346 | } else 347 | $previous_null = false; 348 | $string .= $char; 349 | } 350 | return $string; 351 | } 352 | 353 | /** 354 | * @return array 355 | */ 356 | protected function readBox() { 357 | $box = $this->stream->readGroup('box_header'); 358 | if ($box['size'] == 1) 359 | $box['extended_size'] = $this->stream->readInteger(64); 360 | if ($box['type'] === 'uuid') 361 | $box['usertype'] = $this->stream->readString(16); 362 | return $box; 363 | } 364 | } 365 | -------------------------------------------------------------------------------- /src/Adapters/Video/AviAdapter.php: -------------------------------------------------------------------------------- 1 | ContainerAdapter::VIDEO, 37 | 'auds' => ContainerAdapter::AUDIO, 38 | ); 39 | 40 | /** 41 | * AviAdapter constructor. 42 | * 43 | * @param $filename 44 | * 45 | * @throws \wapmorgan\MediaFile\Exceptions\FileAccessException 46 | * @throws \wapmorgan\MediaFile\Exceptions\ParsingException 47 | */ 48 | public function __construct($filename) { 49 | if (!file_exists($filename) || !is_readable($filename)) throw new FileAccessException('File "'.$filename.'" is not available for reading!'); 50 | $this->filename = $filename; 51 | $this->stream = new BinaryStream($filename); 52 | // $this->stream->setEndian(BinaryStream::BIG); 53 | $this->stream->saveGroup('list', array( 54 | 's:list' => 4, 55 | 'i:size' => 32, 56 | 's:tag' => 4, 57 | )); 58 | $this->stream->saveGroup('chunk', array( 59 | 's:tag' => 4, 60 | 'i:size' => 32, 61 | )); 62 | $this->stream->saveGroup('main_avi_header', array( 63 | 'i:microsecPerFrame' => 32, 64 | 'i:maxBytesPerSec' => 32, 65 | 'i:paddingGranularity' => 32, 66 | 'i:flags' => 32, 67 | 'i:totalFrames' => 32, 68 | 'i:initialFrames' => 32, 69 | 'i:streamsCount' => 32, 70 | 'i:_' => 32, 71 | 'i:width' => 32, 72 | 'i:height' => 32, 73 | 's:_' => 16, 74 | )); 75 | $this->stream->saveGroup('stream_header', array( 76 | 's:type' => 4, 77 | 's:codec' => 4, 78 | 'i:flags' => 32, 79 | 'i:priority' => 16, 80 | 'i:language' => 16, 81 | 'i:initialFrames' => 32, 82 | 'i:scale' => 32, 83 | 'i:rate' => 32, 84 | 'i:start' => 32, 85 | 'i:length' => 32, 86 | 'i:_' => 32, 87 | 'i:quality' => 32, 88 | 'i:sampleSize' => 32, 89 | 'i:position' => 64, 90 | )); 91 | $this->stream->saveGroup('video_strf', array( 92 | 'i:size2' => 32, 93 | 'i:width' => 32, 94 | 'i:height' => 32, 95 | 'i:planes' => 16, 96 | 'i:bitCount' => 16, 97 | 's:compression' => 4, 98 | 'i:sizeImage' => 32, 99 | 'i:xPixelsPerMeter' => 32, 100 | 'i:yPixelsPerMeter' => 32, 101 | 'i:clrUsed' => 32, 102 | 'i:clrImportant' => 32, 103 | )); 104 | $this->stream->saveGroup('video_properties', array( 105 | 'i:format' => 32, 106 | 'i:standard' => 32, 107 | 'i:verticalRefreshRate' => 32, 108 | 'i:hTotal' => 32, 109 | 'i:vTotal' => 32, 110 | 'i:aspect' => 32, 111 | 'i:width' => 32, 112 | 'i:height' => 32, 113 | 'i:fieldPerFrame' => 32, 114 | )); 115 | $this->scan(); 116 | } 117 | 118 | /** 119 | * @throws \wapmorgan\MediaFile\Exceptions\ParsingException 120 | */ 121 | protected function scan() { 122 | $list = $this->stream->readGroup('list'); 123 | // initial list (RIFF or LIST) 124 | if (!in_array($list['list'], array('RIFF', 'LIST'))) 125 | throw new ParsingException('Avi file should start with a list!'); 126 | // following LIST 127 | $next = $this->stream->readGroup('list'); 128 | if (!$next['list'] == 'LIST' || !$next['tag'] == 'hdrl') 129 | throw new ParsingException('Avi does not have header list!'); 130 | 131 | // avih 132 | $avih = $this->stream->readGroup('chunk'); 133 | $avih += $this->stream->readGroup('main_avi_header'); 134 | $this->avih = $avih; 135 | // var_dump($avih); 136 | 137 | // scan for `strl` for every stream 138 | for ($i = 0; $i < $avih['streamsCount']; $i++) { 139 | $strl = $this->stream->readGroup('list'); 140 | // var_dump($strl); 141 | if ($strl['list'] != 'LIST' || $strl['tag'] != 'strl') 142 | throw new ParsingException('Here should be "strl" tag!'); 143 | $this->stream->mark('stream_'.$i.'_start'); 144 | 145 | // strh 146 | $strh = $this->stream->readGroup('chunk'); 147 | $strh += $this->stream->readGroup('stream_header'); 148 | // var_dump($strh); 149 | 150 | // add only supported stream types 151 | if (isset(self::$streamTypes[$strh['type']])) { 152 | $this->streams[$i] = array( 153 | 'type' => self::$streamTypes[$strh['type']], 154 | 'codec' => $strh['codec'], 155 | 'length' => $strh['length'] / $strh['rate'] / $strh['scale'], 156 | ); 157 | 158 | if ($strh['type'] == 'vids') { 159 | if (empty($this->length)) { 160 | $this->streams[$i]['framerate'] = $strh['rate'] / $strh['scale']; 161 | $this->length = $this->streams[$i]['length']; 162 | $this->framerate = $this->streams[$i]['framerate']; 163 | } 164 | 165 | } 166 | } 167 | 168 | // strf 169 | $strf = $this->stream->readGroup('chunk'); 170 | if ($strh['type'] == 'vids') { 171 | $strf += $this->stream->readGroup('video_strf'); 172 | $this->streams[$i]['width'] = $strf['width']; 173 | $this->streams[$i]['height'] = $strf['height']; 174 | // $this->stream->skip($strf['size']); 175 | // var_dump($strf); 176 | } 177 | 178 | // strn 179 | if ($this->stream->compare(4, 'strn')) { 180 | $strn = $this->stream->readGroup('chunk'); 181 | } 182 | 183 | // indx 184 | if ($this->stream->compare(4, 'indx')) { 185 | $strn = $this->stream->readGroup('chunk'); 186 | } 187 | 188 | if ($this->stream->compare(4, 'vprp')) { 189 | $vprp = $this->stream->readGroup('chunk'); 190 | $vprp += $this->stream->readGroup('video_properties'); 191 | // var_dump($vprp); 192 | } 193 | 194 | // go to next strl 195 | $this->stream->go('stream_'.$i.'_start'); 196 | $this->stream->skip($strl['size'] - 4); 197 | } 198 | 199 | $info = $this->stream->readGroup('list'); 200 | // var_dump($info); 201 | } 202 | 203 | public function getLength() { 204 | return $this->length; 205 | } 206 | 207 | public function getWidth() { 208 | return $this->avih['width']; 209 | } 210 | 211 | public function getHeight() { 212 | return $this->avih['height']; 213 | } 214 | 215 | public function getFrameRate() { 216 | return $this->framerate; 217 | } 218 | 219 | public function countStreams() { 220 | return count($this->streams); 221 | } 222 | 223 | public function getStreams() { 224 | return $this->streams; 225 | } 226 | 227 | public function countVideoStreams() { 228 | $count = 0; 229 | foreach ($this->streams as $stream) 230 | if ($stream['type'] == ContainerAdapter::VIDEO) $count++; 231 | return $count; 232 | } 233 | 234 | public function countAudioStreams() { 235 | $count = 0; 236 | foreach ($this->streams as $stream) 237 | if ($stream['type'] == ContainerAdapter::AUDIO) $count++; 238 | return $count; 239 | } 240 | } 241 | -------------------------------------------------------------------------------- /src/Adapters/Video/MkvAdapter.php: -------------------------------------------------------------------------------- 1 | duration; 15 | } 16 | 17 | /** 18 | * @return int 19 | */ 20 | public function getWidth() { 21 | foreach ($this->streams as $stream) { 22 | if ($stream['type'] == ContainerAdapter::VIDEO) 23 | return $stream['width']; 24 | } 25 | } 26 | 27 | /** 28 | * @return int 29 | */ 30 | public function getHeight() { 31 | foreach ($this->streams as $stream) { 32 | if ($stream['type'] == ContainerAdapter::VIDEO) 33 | return $stream['height']; 34 | } 35 | } 36 | 37 | /** 38 | * @return int 39 | */ 40 | public function getFrameRate() { 41 | foreach ($this->streams as $stream) { 42 | if ($stream['type'] == ContainerAdapter::VIDEO) 43 | return $stream['framerate']; 44 | } 45 | } 46 | 47 | /** 48 | * @return int 49 | */ 50 | public function countStreams() { 51 | return count($this->streams); 52 | } 53 | 54 | /** 55 | * @return int 56 | */ 57 | public function countVideoStreams() { 58 | $count = 0; 59 | foreach ($this->streams as $stream) 60 | if ($stream['type'] == ContainerAdapter::VIDEO) $count++; 61 | return $count; 62 | } 63 | 64 | /** 65 | * @return int 66 | */ 67 | public function countAudioStreams() { 68 | $count = 0; 69 | foreach ($this->streams as $stream) 70 | if ($stream['type'] == ContainerAdapter::AUDIO) $count++; 71 | return $count; 72 | } 73 | 74 | /** 75 | * @return array 76 | */ 77 | public function getStreams() { 78 | return $this->streams; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/Adapters/Video/Mp4Adapter.php: -------------------------------------------------------------------------------- 1 | mvhd['duration'] / $this->mvhd['timescale']; 15 | } 16 | 17 | /** 18 | * @return int 19 | */ 20 | public function getWidth() { 21 | foreach ($this->streams as $stream) { 22 | if ($stream['type'] == ContainerAdapter::VIDEO) 23 | return $stream['width']; 24 | } 25 | } 26 | 27 | /** 28 | * @return int 29 | */ 30 | public function getHeight() { 31 | foreach ($this->streams as $stream) { 32 | if ($stream['type'] == ContainerAdapter::VIDEO) 33 | return $stream['height']; 34 | } 35 | } 36 | 37 | /** 38 | * @return int 39 | */ 40 | public function getFrameRate() { 41 | foreach ($this->streams as $stream) { 42 | if ($stream['type'] == ContainerAdapter::VIDEO) 43 | return $stream['framerate']; 44 | } 45 | } 46 | 47 | /** 48 | * @return int 49 | */ 50 | public function countStreams() { 51 | return count($this->streams); 52 | } 53 | 54 | /** 55 | * @return int 56 | */ 57 | public function countVideoStreams() { 58 | $count = 0; 59 | foreach ($this->streams as $stream) 60 | if ($stream['type'] == ContainerAdapter::VIDEO) $count++; 61 | return $count; 62 | } 63 | 64 | /** 65 | * @return int 66 | */ 67 | public function countAudioStreams() { 68 | $count = 0; 69 | foreach ($this->streams as $stream) 70 | if ($stream['type'] == ContainerAdapter::AUDIO) $count++; 71 | return $count; 72 | } 73 | 74 | /** 75 | * @return array 76 | */ 77 | public function getStreams() { 78 | return $this->streams; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/Adapters/Video/WmvAdapter.php: -------------------------------------------------------------------------------- 1 | length = $this->properties['send_length']; 24 | foreach ($this->streams as $stream) { 25 | if ($stream['type'] == ContainerAdapter::VIDEO && empty($this->width)) { 26 | $this->width = $stream['width']; 27 | $this->height = $stream['height']; 28 | $this->framerate = $stream['framerate']; 29 | break; 30 | } 31 | } 32 | } 33 | 34 | /** 35 | * @return int 36 | */ 37 | public function getLength() { 38 | return $this->length; 39 | } 40 | 41 | /** 42 | * @return int 43 | */ 44 | public function getWidth() { 45 | return $this->width; 46 | } 47 | 48 | /** 49 | * @return int 50 | */ 51 | public function getHeight() { 52 | return $this->height; 53 | } 54 | 55 | /** 56 | * @return int 57 | */ 58 | public function getFrameRate() { 59 | return $this->framerate; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/Adapters/VideoAdapter.php: -------------------------------------------------------------------------------- 1 | WavAdapter::class, 28 | Detector::MP3 => Mp3Adapter::class, 29 | Detector::FLAC => FlacAdapter::class, 30 | Detector::AAC => AacAdapter::class, 31 | Detector::OGG => OggAdapter::class, 32 | Detector::AMR => AmrAdapter::class, 33 | Detector::WMA => WmaAdapter::class, 34 | Detector::AVI => AviAdapter::class, 35 | Detector::ASF => AsfAdapter::class, 36 | Detector::WMV => WmvAdapter::class, 37 | Detector::MP4 => Mp4Adapter::class, 38 | Detector::MKV => MkvAdapter::class, 39 | ]; 40 | 41 | /** @var string */ 42 | protected $filename; 43 | protected $type; 44 | 45 | /** @var string */ 46 | protected $format; 47 | 48 | /** @var AudioAdapter|VideoAdapter */ 49 | public $adapter; 50 | 51 | /** 52 | * @param string $filename 53 | * @return MediaFile 54 | * @throws FileAccessException 55 | */ 56 | static public function open($filename) 57 | { 58 | if (!file_exists($filename) || !is_readable($filename)) 59 | throw new FileAccessException('File "'.$filename.'" is not available for reading!'); 60 | 61 | $type = Detector::detectByFilename($filename) ?: Detector::detectByContent($filename); 62 | 63 | if ($type === false) 64 | throw new FileAccessException('Unknown format for file "'.$filename.'"!'); 65 | 66 | if (!isset(self::$formatHandlers[$type[1]])) 67 | throw new FileAccessException('File "'.$filename.'" is not supported, it\'s "'.$type[0].'/'.$type[1].'"!'); 68 | 69 | return new self($filename, $type[1]); 70 | } 71 | 72 | /** 73 | * MediaFile constructor. 74 | * 75 | * @param string $filename 76 | * @param string $format 77 | * @throws FileAccessException 78 | */ 79 | public function __construct($filename, $format) { 80 | if (!file_exists($filename) || !is_readable($filename)) throw new Exceptions\FileAccessException('File "'.$filename.'" is not available for reading!'); 81 | 82 | if (!isset(self::$formatHandlers[$format])) 83 | throw new FileAccessException('Format "'.$format.'" does not have a handler!'); 84 | 85 | $adapter_class = self::$formatHandlers[$format]; 86 | $this->adapter = new $adapter_class($filename); 87 | 88 | $this->filename = $filename; 89 | $this->format = $format; 90 | } 91 | 92 | /** 93 | * @return bool 94 | */ 95 | public function isAudio() { 96 | return $this->adapter instanceof AudioAdapter; 97 | } 98 | 99 | /** 100 | * @return bool 101 | */ 102 | public function isVideo() { 103 | return $this->adapter instanceof VideoAdapter; 104 | } 105 | 106 | /** 107 | * @return bool 108 | */ 109 | public function isContainer() { 110 | return $this->adapter instanceof ContainerAdapter; 111 | } 112 | 113 | /** 114 | * @return string 115 | */ 116 | public function getFormat() { 117 | return $this->format; 118 | } 119 | 120 | /** 121 | * @return AudioAdapter 122 | */ 123 | public function getAudio() { 124 | return $this->adapter; 125 | } 126 | 127 | /** 128 | * @return VideoAdapter 129 | * @throws FileAccessException 130 | */ 131 | public function getVideo() { 132 | return $this->adapter; 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /tests/audios/AacAudioTest.php: -------------------------------------------------------------------------------- 1 | assertInstanceOf('wapmorgan\MediaFile\MediaFile', $file); 10 | $this->assertTrue($file->isAudio()); 11 | 12 | // checking audio interfac 13 | $audio = $file->getAudio(); 14 | $this->assertInstanceOf('wapmorgan\MediaFile\Adapters\AudioAdapter', $audio); 15 | $this->assertEquals(34, $audio->getLength(), '', 2); 16 | $this->assertEquals(44100, $audio->getSampleRate()); 17 | $this->assertEquals(2, $audio->getChannels()); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /tests/bootstrap.php: -------------------------------------------------------------------------------- 1 | open($tmp_file) !== true) 19 | throw new Exception('Can\'t open downloaded fixtures as ZIP archive ('.$tmp_file.')'); 20 | 21 | if ($zip->extractTo(FIXTURES_DIR) !== true) 22 | throw new Exception('Can\'t extract downloaded fixtures as ZIP archive ('.$tmp_file.')'); 23 | 24 | echo 'Extracted fixtures: '.$zip->numFiles.' file(s)'.PHP_EOL; 25 | 26 | $zip->close(); 27 | 28 | unlink($tmp_file); 29 | } 30 | --------------------------------------------------------------------------------