Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 241
0.00% covered (danger)
0.00%
0 / 10
CRAP
0.00% covered (danger)
0.00%
0 / 1
File_Ogg
0.00% covered (danger)
0.00%
0 / 225
0.00% covered (danger)
0.00%
0 / 10
4556
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
20
 _littleEndianBin2Hex
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
6
 _readBigEndian
0.00% covered (danger)
0.00%
0 / 36
0.00% covered (danger)
0.00%
0 / 1
110
 _readLittleEndian
0.00% covered (danger)
0.00%
0 / 34
0.00% covered (danger)
0.00%
0 / 1
110
 _decodePageHeader
0.00% covered (danger)
0.00%
0 / 53
0.00% covered (danger)
0.00%
0 / 1
110
 _splitStreams
0.00% covered (danger)
0.00%
0 / 52
0.00% covered (danger)
0.00%
0 / 1
306
 getStream
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 listStreams
0.00% covered (danger)
0.00%
0 / 24
0.00% covered (danger)
0.00%
0 / 1
90
 getStartOffset
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 getLength
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2/* vim: set expandtab tabstop=4 shiftwidth=4: */
3// +----------------------------------------------------------------------------+
4// | File_Ogg PEAR Package for Accessing Ogg Bitstreams                         |
5// | Copyright (c) 2005-2007                                                    |
6// | David Grant <david@grant.org.uk>                                           |
7// | Tim Starling <tstarling@wikimedia.org>                                     |
8// +----------------------------------------------------------------------------+
9// | This library is free software; you can redistribute it and/or              |
10// | modify it under the terms of the GNU Lesser General Public                 |
11// | License as published by the Free Software Foundation; either               |
12// | version 2.1 of the License, or (at your option) any later version.         |
13// |                                                                            |
14// | This library is distributed in the hope that it will be useful,            |
15// | but WITHOUT ANY WARRANTY; without even the implied warranty of             |
16// | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU          |
17// | Lesser General Public License for more details.                            |
18// |                                                                            |
19// | You should have received a copy of the GNU Lesser General Public           |
20// | License along with this library; if not, write to the Free Software        |
21// | Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA |
22// +----------------------------------------------------------------------------+
23
24/**
25 * @author      David Grant <david@grant.org.uk>, Tim Starling <tstarling@wikimedia.org>
26 * @category    File
27 * @copyright   David Grant <david@grant.org.uk>, Tim Starling <tstarling@wikimedia.org>
28 * @license     http://www.gnu.org/copyleft/lesser.html GNU LGPL
29 * @link        http://pear.php.net/package/File_Ogg
30 * @package     File_Ogg
31 * @version     CVS: $Id: Ogg.php,v 1.14 2005/11/19 09:06:30 djg Exp $
32 */
33
34/**
35 * @access  public
36 */
37
38use MediaWiki\TimedMediaHandler\Handlers\OggHandler\OggException;
39
40define("OGG_STREAM_VORBIS",     1);
41/**
42 * @access  public
43 */
44define("OGG_STREAM_THEORA",     2);
45/**
46 * @access  public
47 */
48define("OGG_STREAM_SPEEX",      3);
49/**
50 * @access  public
51 */
52define("OGG_STREAM_FLAC",       4);
53/**
54 * @access  public
55 */
56define("OGG_STREAM_OPUS",       5);
57
58/**
59 * Capture pattern to determine if a file is an Ogg physical stream.
60 *
61 * @access  private
62 */
63define("OGG_CAPTURE_PATTERN", "OggS");
64/**
65 * Maximum size of an Ogg stream page plus four.  This value is specified to allow
66 * efficient parsing of the physical stream.  The extra four is a paranoid measure
67 * to make sure a capture pattern is not split into two parts accidentally.
68 *
69 * @access  private
70 */
71define("OGG_MAXIMUM_PAGE_SIZE", 65311);
72/**
73 * Capture pattern for an Ogg Vorbis logical stream.
74 *
75 * @access  private
76 */
77define("OGG_STREAM_CAPTURE_VORBIS", "vorbis");
78/**
79 * Capture pattern for an Ogg Speex logical stream.
80 * @access  private
81 */
82define("OGG_STREAM_CAPTURE_SPEEX",  "Speex   ");
83/**
84 * Capture pattern for an Ogg FLAC logical stream.
85 *
86 * @access  private
87 */
88define("OGG_STREAM_CAPTURE_FLAC",   "FLAC");
89/**
90 * Capture pattern for an Ogg Theora logical stream.
91 *
92 * @access  private
93 */
94define("OGG_STREAM_CAPTURE_THEORA", "theora");
95/**
96 * Capture pattern for an Ogg Opus logical stream.
97 * @access  private
98 */
99define("OGG_STREAM_CAPTURE_OPUS",  "OpusHead");
100/**
101 * Error thrown if the file location passed is nonexistant or unreadable.
102 *
103 * @access  private
104 */
105define("OGG_ERROR_INVALID_FILE", 1);
106/**
107 * Error thrown if the user attempts to extract an unsupported logical stream.
108 *
109 * @access  private
110 */
111define("OGG_ERROR_UNSUPPORTED",  2);
112/**
113 * Error thrown if the user attempts to extract an logical stream with no
114 * corresponding serial number.
115 *
116 * @access  private
117 */
118define("OGG_ERROR_BAD_SERIAL",   3);
119/**
120 * Error thrown if the stream appears to be corrupted.
121 *
122 * @access  private
123 */
124define("OGG_ERROR_UNDECODABLE",      4);
125
126
127/**
128 * Class for parsing a ogg bitstream.
129 *
130 * This class provides a means to access several types of logical bitstreams (e.g. Vorbis)
131 * within a Ogg physical bitstream.
132 *
133 * @link    http://www.xiph.org/ogg/doc/
134 * @package File_Ogg
135 */
136class File_Ogg
137{
138    /**
139     * File pointer to Ogg container.
140     *
141     * This is the file pointer used for extracting data from the Ogg stream.  It is
142     * the result of a standard fopen call.
143     *
144     * @var     resource
145     * @access  private
146     */
147    var $_filePointer;
148
149    /**
150     * The container for all logical streams.
151     *
152     * List of all of the unique streams in the Ogg physical stream.  The key
153     * used is the unique serial number assigned to the logical stream by the
154     * encoding application.
155     *
156     * @var     array
157     * @access  private
158     */
159    var $_streamList = array();
160    var $_streams = array();
161
162    /**
163     * Length in seconds of each stream group
164     */
165    var $_groupLengths = array();
166
167    /**
168     * Total length in seconds of the entire file
169     */
170    var $_totalLength;
171    var $_startOffset = false;
172
173    /**
174     * Maximum number of pages to store detailed metadata for, per stream.
175     * We can't store every page because there could be millions, causing an OOM.
176     * This must be big enough so that all the codecs can get the metadata they
177     * need without re-reading the file.
178     */
179    var $_maxPageCacheSize = 4;
180
181    /**
182     * Returns an interface to an Ogg physical stream.
183     *
184     * This method takes the path to a local file and examines it for a physical
185     * ogg bitstream.  After instantiation, the user should query the object for
186     * the logical bitstreams held within the ogg container.
187     *
188     * @access  public
189     * @param   string  $fileLocation   The path of the file to be examined.
190     */
191    function __construct($fileLocation)
192    {
193        clearstatcache();
194        if (! file_exists($fileLocation)) {
195            throw new OggException("Couldn't Open File. Check File Path.", OGG_ERROR_INVALID_FILE);
196        }
197
198        // Open this file as a binary, and split the file into streams.
199        $this->_filePointer = fopen($fileLocation, "rb");
200        if (!$this->_filePointer)
201            throw new OggException("Couldn't Open File. Check File Permissions.", OGG_ERROR_INVALID_FILE);
202
203        // Check for a stream at the start
204        $magic = fread($this->_filePointer, strlen(OGG_CAPTURE_PATTERN));
205        if ($magic !== OGG_CAPTURE_PATTERN) {
206            throw new OggException("Couldn't read file: Incorrect magic number.", OGG_ERROR_UNDECODABLE);
207        }
208        fseek($this->_filePointer, 0, SEEK_SET);
209
210        $this->_splitStreams();
211        fclose($this->_filePointer);
212    }
213
214    /**
215     * Little-endian equivalent for bin2hex
216     * @static
217     */
218    static function _littleEndianBin2Hex( $bin ) {
219        $bigEndian = bin2hex( $bin );
220        // Reverse entire string
221        $reversed = strrev( $bigEndian );
222        // Swap nibbles back
223        for ( $i = 0; $i < strlen( $bigEndian ); $i += 2 ) {
224            $temp = $reversed[$i];
225            $reversed[$i] = $reversed[$i+1];
226            $reversed[$i+1] = $temp;
227        }
228        return $reversed;
229    }
230
231
232    /**
233     * Read a binary structure from a file. An array of unsigned integers are read.
234     * Large integers are upgraded to floating point on overflow.
235     *
236     * Format is big-endian as per Theora bit packing convention, this function
237     * won't work for Vorbis.
238     *
239     * @param   resource    $file
240     * @param   array       $fields Associative array mapping name to length in bits
241     */
242    static function _readBigEndian($file, $fields)
243    {
244        $bufferLength = ceil(array_sum($fields) / 8);
245        $buffer = fread($file, $bufferLength);
246        if (strlen($buffer) != $bufferLength) {
247            throw new OggException('Unexpected end of file', OGG_ERROR_UNDECODABLE);
248        }
249        $bytePos = 0;
250        $bitPos = 0;
251        $byteValue = ord($buffer[0]);
252        $output = array();
253        foreach ($fields as $name => $width) {
254            if ($width % 8 == 0 && $bitPos == 0) {
255                // Byte aligned case
256                $bytes = $width / 8;
257                $endBytePos = $bytePos + $bytes;
258                $value = 0;
259                while ($bytePos < $endBytePos) {
260                    $value = ($value * 256) + ord($buffer[$bytePos]);
261                    $bytePos++;
262                }
263                if ($bytePos < strlen($buffer)) {
264                    $byteValue = ord($buffer[$bytePos]);
265                }
266            } else {
267                // General case
268                $bitsRemaining = $width;
269                $value = 0;
270                while ($bitsRemaining > 0) {
271                    $bitsToRead = min($bitsRemaining, 8 - $bitPos);
272                    $byteValue <<= $bitsToRead;
273                    $overflow = ($byteValue & 0xff00) >> 8;
274                    $byteValue &= $byteValue & 0xff;
275
276                    $bitPos += $bitsToRead;
277                    $bitsRemaining -= $bitsToRead;
278                    $value += $overflow * pow(2, $bitsRemaining);
279
280                    if ($bitPos >= 8) {
281                        $bitPos = 0;
282                        $bytePos++;
283                        if ($bitsRemaining <= 0) {
284                            break;
285                        }
286                        $byteValue = ord($buffer[$bytePos]);
287                    }
288                }
289            }
290            $output[$name] = $value;
291            assert($bytePos <= $bufferLength);
292        }
293        return $output;
294    }
295
296    /**
297     * Read a binary structure from a file. An array of unsigned integers are read.
298     * Large integers are upgraded to floating point on overflow.
299     *
300     * Format is little-endian as per Vorbis bit packing convention.
301     *
302     * @param   resource    $file
303     * @param   array       $fields Associative array mapping name to length in bits
304     */
305    static function _readLittleEndian( $file, $fields ) {
306        $bufferLength = ceil(array_sum($fields) / 8);
307        $buffer = fread($file, $bufferLength);
308        if (strlen($buffer) != $bufferLength) {
309            throw new OggException('Unexpected end of file', OGG_ERROR_UNDECODABLE);
310        }
311
312        $bytePos = 0;
313        $bitPos = 0;
314        $byteValue = ord($buffer[0]) << 8;
315        $output = array();
316        foreach ($fields as $name => $width) {
317            if ($width % 8 == 0 && $bitPos == 0) {
318                // Byte aligned case
319                $bytes = $width / 8;
320                $value = 0;
321                for ($i = 0; $i < $bytes; $i++, $bytePos++) {
322                    $value += pow(256, $i) * ord($buffer[$bytePos]);
323                }
324                if ($bytePos < strlen($buffer)) {
325                    $byteValue = ord($buffer[$bytePos]) << 8;
326                }
327            } else {
328                // General case
329                $bitsRemaining = $width;
330                $value = 0;
331                while ($bitsRemaining > 0) {
332                    $bitsToRead = min($bitsRemaining, 8 - $bitPos);
333                    $byteValue >>= $bitsToRead;
334                    $overflow = ($byteValue & 0xff) >> (8 - $bitsToRead);
335                    $byteValue &= 0xff00;
336
337                    $value += $overflow * pow(2, $width - $bitsRemaining);
338                    $bitPos += $bitsToRead;
339                    $bitsRemaining -= $bitsToRead;
340
341                    if ($bitPos >= 8) {
342                        $bitPos = 0;
343                        $bytePos++;
344                        if ($bitsRemaining <= 0) {
345                            break;
346                        }
347                        $byteValue = ord($buffer[$bytePos]) << 8;
348                    }
349                }
350            }
351            $output[$name] = $value;
352            assert($bytePos <= $bufferLength);
353        }
354        return $output;
355    }
356
357
358    /**
359     * @access  private
360     */
361    function _decodePageHeader($pageData, $pageOffset, $groupId)
362    {
363        // Don't blindly substr() and unpack() if data is cut off
364        if (strlen($pageData) < 27)
365            return (false);
366
367        // Extract the various bits and pieces found in each packet header.
368        if (substr($pageData, 0, 4) != OGG_CAPTURE_PATTERN)
369            return (false);
370
371        $stream_version = unpack("C1data", substr($pageData, 4, 1));
372        if ($stream_version['data'] != 0x00)
373            return (false);
374
375        $header_flag     = unpack("Cdata", substr($pageData, 5, 1));
376
377        // Exact granule position
378        $abs_granule_pos = self::_littleEndianBin2Hex( substr($pageData, 6, 8));
379
380        // Approximate (floating point) granule position
381        $pos = unpack("Va/Vb", substr($pageData, 6, 8));
382        $approx_granule_pos = $pos['a'] + $pos['b'] * pow(2, 32);
383
384        // Serial number for the current datastream.
385        $stream_serial   = unpack("Vdata", substr($pageData, 14, 4));
386        $page_sequence   = unpack("Vdata", substr($pageData, 18, 4));
387        $checksum        = unpack("Vdata", substr($pageData, 22, 4));
388        $page_segments   = unpack("Cdata", substr($pageData, 26, 1));
389
390        // Header is extended with segment lengths; make sure we have data.
391        if (strlen($pageData) < 27 + $page_segments['data'])
392            return (false);
393
394        $segments_total  = 0;
395        for ($i = 0; $i < $page_segments['data']; ++$i) {
396            $segment_length = unpack("Cdata", substr($pageData, 26 + ($i + 1), 1));
397            $segments_total += $segment_length['data'];
398        }
399        $pageFinish = $pageOffset + 27 + $page_segments['data'] + $segments_total;
400        $page = array(
401            'stream_version'        => $stream_version['data'],
402            'header_flag'           => $header_flag['data'],
403            'abs_granule_pos'       => $abs_granule_pos,
404            'approx_granule_pos'    => $approx_granule_pos,
405            'checksum'              => sprintf("%u", $checksum['data']),
406            'segments'              => $page_segments['data'],
407            'head_offset'           => $pageOffset,
408            'body_offset'           => $pageOffset + 27 + $page_segments['data'],
409            'body_finish'           => $pageFinish,
410            'data_length'           => $pageFinish - $pageOffset,
411            'group'                 => $groupId,
412        );
413        if ( !isset( $this->_streamList[$stream_serial['data']] ) ) {
414            $this->_streamList[$stream_serial['data']] = array(
415                'pages' => array(),
416                'data_length' => 0,
417                'first_granule_pos' => null,
418                'last_granule_pos' => null,
419            );
420        }
421        $stream =& $this->_streamList[$stream_serial['data']];
422        if ( count( $stream['pages'] ) < $this->_maxPageCacheSize ) {
423            $stream['pages'][$page_sequence['data']] = $page;
424        }
425        $stream['last_page'] = $page;
426        $stream['data_length'] += $page['data_length'];
427
428        # Reject -1 as a granule pos, that means no segment finished in the packet
429        if ( $abs_granule_pos !== 'ffffffffffffffff' ) {
430            if ( $stream['first_granule_pos'] === null ) {
431                $stream['first_granule_pos'] = $abs_granule_pos;
432            }
433            $stream['last_granule_pos'] = $abs_granule_pos;
434        }
435
436        $pageData = null;
437        return $page;
438    }
439
440    /**
441     *  @access         private
442     */
443    function _splitStreams()
444    {
445        // Loop through the physical stream until there are no more pages to read.
446        $groupId = 0;
447        $openStreams = 0;
448        $this_page_offset = 0;
449        while (!feof($this->_filePointer)) {
450            $pageData = fread($this->_filePointer, 282);
451            if (strval($pageData) === '') {
452                break;
453            }
454            $page = $this->_decodePageHeader($pageData, $this_page_offset, $groupId);
455            if ($page === false) {
456                throw new OggException("Cannot decode Ogg file: Invalid page at offset $this_page_offset", OGG_ERROR_UNDECODABLE);
457            }
458
459            // Keep track of multiplexed groups
460            if ($page['header_flag'] & 2/*bos*/) {
461                $openStreams++;
462            } elseif ($page['header_flag'] & 4/*eos*/) {
463                $openStreams--;
464                if (!$openStreams) {
465                    // End of group
466                    $groupId++;
467                }
468            }
469            if ($openStreams < 0) {
470                throw new OggException("Unexpected end of stream", OGG_ERROR_UNDECODABLE);
471            }
472
473            $this_page_offset = $page['body_finish'];
474            fseek( $this->_filePointer, $this_page_offset, SEEK_SET );
475        }
476        // Loop through the streams, and find out what type of stream is available.
477        $groupLengths = array();
478        foreach ($this->_streamList as $stream_serial => $streamData) {
479            // @phan-suppress-next-line PhanTypeArraySuspiciousNullable False positive
480            fseek($this->_filePointer, $streamData['pages'][0]['body_offset'], SEEK_SET);
481            $pattern = fread($this->_filePointer, 8);
482            if (preg_match("/" . OGG_STREAM_CAPTURE_VORBIS . "/", $pattern)) {
483                $this->_streamList[$stream_serial]['stream_type'] = OGG_STREAM_VORBIS;
484                $stream = new File_Ogg_Vorbis($stream_serial, $streamData, $this->_filePointer);
485            } elseif (preg_match("/" . OGG_STREAM_CAPTURE_SPEEX . "/", $pattern)) {
486                $this->_streamList[$stream_serial]['stream_type'] = OGG_STREAM_SPEEX;
487                $stream = new File_Ogg_Speex($stream_serial, $streamData, $this->_filePointer);
488            } elseif (preg_match("/" . OGG_STREAM_CAPTURE_FLAC . "/", $pattern)) {
489                $this->_streamList[$stream_serial]['stream_type'] = OGG_STREAM_FLAC;
490                $stream = new File_Ogg_Flac($stream_serial, $streamData, $this->_filePointer);
491            } elseif (preg_match("/" . OGG_STREAM_CAPTURE_THEORA . "/", $pattern)) {
492                $this->_streamList[$stream_serial]['stream_type'] = OGG_STREAM_THEORA;
493                $stream = new File_Ogg_Theora($stream_serial, $streamData, $this->_filePointer);
494            } elseif (preg_match("/" . OGG_STREAM_CAPTURE_OPUS . "/", $pattern)) {
495                $this->_streamList[$stream_serial]['stream_type'] = OGG_STREAM_OPUS;
496                $stream = new File_Ogg_Opus($stream_serial, $streamData, $this->_filePointer);
497            } else {
498                $streamData['stream_type'] = "unknown";
499                $stream = false;
500            }
501
502            if ($stream) {
503                $this->_streams[$stream_serial] = $stream;
504                // @phan-suppress-next-line PhanTypeArraySuspiciousNullable False positive
505                $group = $streamData['pages'][0]['group'];
506                if (isset($groupLengths[$group])) {
507                    $groupLengths[$group] = max($groupLengths[$group], $stream->getLength());
508                } else {
509                    $groupLengths[$group] = $stream->getLength();
510                }
511                //just store the startOffset for the first stream:
512                if( $this->_startOffset === false ){
513                    $this->_startOffset = $stream->getStartOffset();
514                }
515
516            }
517        }
518        $this->_groupLengths = $groupLengths;
519        $this->_totalLength = array_sum( $groupLengths );
520        $this->_streamList = [];
521    }
522
523    /**
524     * Returns the appropriate logical bitstream that corresponds to the provided serial.
525     *
526     * This function returns a logical bitstream contained within the Ogg physical
527     * stream, corresponding to the serial used as the offset for that bitstream.
528     * The returned stream may be Vorbis, Speex, FLAC or Theora, although the only
529     * usable bitstream is Vorbis.
530     *
531     * @return File_Ogg_Bitstream
532     */
533    function &getStream($streamSerial)
534    {
535        if (! array_key_exists($streamSerial, $this->_streams))
536                throw new OggException("The stream number is invalid.", OGG_ERROR_BAD_SERIAL);
537
538        return $this->_streams[$streamSerial];
539    }
540
541    /**
542     * Returns an array of logical streams inside this physical bitstream.
543     *
544     * This function returns an array of logical streams found within this physical
545     * bitstream.  If a filter is provided, only logical streams of the requested type
546     * are returned, as an array of serial numbers.  If no filter is provided, this
547     * function returns a two-dimensional array, with the stream type as the primary key,
548     * and a value consisting of an array of stream serial numbers.
549     *
550     * @param  int      $filter
551     * @return array
552     */
553    function listStreams($filter = null)
554    {
555        $streams = array();
556        // Loops through the streams and assign them to an appropriate index,
557        // ready for filtering the second part of this function.
558        foreach ($this->_streams as $serial => $stream) {
559            $stream_type = 0;
560            switch (get_class($stream)) {
561                case "file_ogg_flac":
562                    $stream_type = OGG_STREAM_FLAC;
563                    break;
564                case "file_ogg_speex":
565                    $stream_type = OGG_STREAM_SPEEX;
566                    break;
567                case "file_ogg_theora":
568                    $stream_type = OGG_STREAM_THEORA;
569                    break;
570                case "file_ogg_vorbis":
571                    $stream_type = OGG_STREAM_VORBIS;
572                    break;
573            }
574            if (! isset($streams[$stream_type]))
575                // Initialise the result list for this stream type.
576                $streams[$stream_type] = array();
577
578            $streams[$stream_type][] = $serial;
579        }
580
581        // Perform filtering.
582        if (is_null($filter))
583            return ($streams);
584        elseif (isset($streams[$filter]))
585            return ($streams[$filter]);
586        else
587            return array();
588    }
589    /**
590     * getStartOffset
591     *
592     * @return int
593     */
594    function getStartOffset(){
595        if( $this->_startOffset === false)
596            return 0;
597        return $this->_startOffset;
598    }
599    /**
600     * Get the total length of the group of streams
601     */
602    function getLength() {
603        return $this->_totalLength;
604    }
605}
606?>