file = fopen( $filename, 'rb' ); if ( !$this->file ) { throw new Exception( 'Failed to open MP4 input file' ); } } function __destruct() { if ( $this->file ) { fclose( $this->file ); $this->file = null; } } /** * @return int */ public function pos() { $pos = ftell( $this->file ); if ( $pos === false ) { throw new Exception( 'Failed to read position in MP4 file' ); } return $pos; } /** * @param int $length number of bytes to read * @return string|false raw bytes or false on eof */ public function readBytes( $length ) { $bytes = fread( $this->file, $length ); if ( feof( $this->file ) ) { return false; } if ( $bytes === false || strlen( $bytes ) < $length ) { throw new Exception( "Failed to read $length bytes from MP4 file" ); } return $bytes; } /** * @param int $length number of bytes to skip over */ private function skipBytes( $length ) { $retval = fseek( $this->file, $length, SEEK_CUR ); if ( $retval < 0 ) { throw new Exception( "Failed to skip ahead $length bytes in MP4 file" ); } } /** * @return int|false 32-bit integer value or false on eof */ public function read32() { $bytes = $this->readBytes( 4 ); if ( $bytes === false ) { return $bytes; } $data = unpack( 'Nval', $bytes ); return $data['val']; } /** * @return string|false 4-byte type code or false on eof */ public function readType() { return $this->readBytes( 4 ); } /** * @param callable $callback in the form function(MP4Box) * @return mixed the return value from the callback, or false on eof */ public function readBox( $callback ) { $start = $this->pos(); $size = $this->read32(); if ( $size === false ) { return false; } $end = $start + $size; $type = $this->readType(); if ( $type === false ) { return false; } $box = new MP4Box( $this, $start, $size, $type ); $retval = call_user_func( $callback, $box ); $remaining = $end - $this->pos(); if ( $remaining > 0 ) { $this->skipBytes( $remaining ); } return $retval; } } class MP4Box { private $reader; public $start; public $size; public $type; public function __construct( MP4Reader $reader, $start, $size, $type ) { $this->reader = $reader; $this->start = $start; $this->size = $size; $this->type = $type; } public function pos() { return $this->reader->pos(); } public function end() { return $this->start + $this->size; } public function remaining() { return $this->end() - $this->pos(); } public function guard( $length ) { $remaining = $this->remaining(); if ( $remaining < $length ) { throw new Exception( "Reading beyond end of box; had $remaining bytes, wanted $length" ); } } public function readBytes( $length ) { $this->guard( $length ); return $this->reader->readBytes( $length ); } public function read32() { $this->guard( 4 ); return $this->reader->read32(); } public function readType() { $this->guard( 4 ); return $this->reader->readType(); } public function readBox( $callback ) { $this->guard( 8 ); return $this->reader->readBox( $callback ); } } function hexdump( $str ) { $out = ''; $len = strlen( $str ); for ( $i = 0; $i < $len; $i++ ) { $char = $str[$i]; $byte = ord( $char ); // really? $digits = str_pad( dechex( $byte ), 2, '0', STR_PAD_LEFT ); $out .= $digits; } return $out; } function safestr( $str ) { $out = ''; $len = strlen( $str ); for ( $i = 0; $i < $len; $i++ ) { $char = $str[$i]; $byte = ord( $char ); if ( $byte >= 32 && $byte <= 126 ) { $out .= $char; } else { $out .= '.'; } } return $out; } /** * @param string $filename - input MP4 file to read * @return array[] - list of segment info */ function extractFragmentedMP4( $filename ) { $segments = []; $mp4 = new MP4Reader( $filename ); $eof = false; $moof = false; $init = false; while ( !$eof ) { $eof = !$mp4->readBox( function ( $box ) use ( &$segments, &$moof, &$init ) { /* Need to: - find the end of the moov; everything up to that is the initialization segment - https://www.w3.org/TR/mse-byte-stream-format-isobmff/#iso-init-segments - find the start of each styp+moof fragment - https://www.w3.org/TR/mse-byte-stream-format-isobmff/#iso-media-segments - find the start timestamp of each moof fragment - find the duration of each moof fragment moov.trak.mdia.mdhd.timescale - looks useful 12288 for 24fps? moov.trak.mdia.mdhd.duration - 0 on the moov moov.mvex.trex.default_sample_duration - 0 on the moov moof.traf.tfhd.default_sample_duration is 512 on these moof.traf.tfdt.baseMediaDecodeTime is 0 on the first, 122880 on the second frag (10 seconds in?) moof.mdat.sample_duration is empty here (uses default I guess?) moof.mdat.sample_composition_time_offset also empty opus has timescale 48000 in moov.trak.mdia.mdhd */ switch ( $box->type ) { case 'ftyp': break; case 'moof': if ( !$init ) { $init = [ 'start' => 0, 'size' => $box->end(), 'timestamp' => 0.0, 'duration' => 0.0, ]; $segments['init'] = $init; } $moof = $box->start; break; case 'mdat': // @todo use timestamp and duration data array_push( $segments, [ 'start' => $moof, 'size' => $box->end() - $moof, 'timestamp' => 0.0, 'duration' => 0.0, ] ); break; default: // ignore } return true; } ); } return $segments; } $argv = $_SERVER['argv']; $self = array_shift( $argv ); $filename = array_shift( $argv ); $segments = extractFragmentedMP4( $filename ); //var_dump( $segments ); foreach ( $segments as $key => $segment ) { if ( $key === 'init' ) { print "$key {$segment['start']},{$segment['size']}\n"; } else { print "$key {$segment['timestamp']},{$segment['duration']} @ {$segment['start']},{$segment['size']}\n"; } }