file = $file; } /** * @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 64-bit integer value or false on eof */ public function read64() { $bytes = $this->readBytes( 8 ); if ( $bytes === false ) { return false; } $data = unpack( 'Jval', $bytes ); return $data['val']; } /** * @return int|false 32-bit integer value or false on eof */ public function read32() { $bytes = $this->readBytes( 4 ); if ( $bytes === false ) { return false; } $data = unpack( 'Nval', $bytes ); return $data['val']; } /** * @return int|false 24-bit integer value or false on eof */ public function read24() { $bytes = $this->readBytes( 3 ); if ( $bytes === false ) { return false; } $data = unpack( 'Nval', "\x00$bytes" ); return $data['val']; } /** * @return int|false 16-bit integer value or false on eof */ public function read16() { $bytes = $this->readBytes( 2 ); if ( $bytes === false ) { return false; } $data = unpack( 'nval', $bytes ); return $data['val']; } /** * @return int|false 8-bit integer value or false on eof */ public function read8() { $bytes = $this->readBytes( 1 ); if ( $bytes === false ) { return false; } return ord( $bytes ); } /** * @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 bool true on success, 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->file, $start, $size, $type ); $retval = call_user_func( $callback, $box ); $remaining = $end - $this->pos(); if ( $remaining > 0 ) { $this->skipBytes( $remaining ); } return true; } /** * Scan a series of boxes and pass control based on type. * Unrecognized boxes will be skipped over. * @param callable[] $map array of callables keyed by fourCC type code */ public function boxes( $map ) { $ok = true; while ( $ok ) { $ok = $this->readBox( function ( $box ) use ( $map ) { $handler = $map[$box->type] ?? false; if ( is_callable( $handler ) ) { call_user_func( $handler, $box ); } else if ( is_array( $handler ) ) { $box->boxes( $handler ); } else if ( $handler === false ) { // no-op } else { throw new Exception( "Unexpected callback or map type for type $box->type" ); } } ); } } } class MP4FileReader extends MP4Reader { /** * @param string $filename */ public function __construct( $filename ) { $file = fopen( $filename, 'rb' ); if ( !$file ) { throw new Exception( 'Failed to open MP4 input file' ); } parent::__construct( $file ); } function __destruct() { if ( $this->file ) { fclose( $this->file ); $this->file = null; } } } class MP4Box extends MP4Reader { public $start; public $size; public $type; public function __construct( $file, $start, $size, $type ) { parent::__construct( $file ); $this->start = $start; $this->size = $size; $this->type = $type; } public function end() { return $this->start + $this->size; } public function remaining() { return $this->end() - $this->pos(); } public function readBytes( $length ) { if ( $length > $this->remaining() ) { return false; } return parent::readBytes( $length ); } } 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 MP4FileReader( $filename ); $eof = false; $moof = false; $timestamp = 0.0; $duration = 0.0; $timescale = 0; $dts = 0; $first_pts = 0; $max_pts = 0; $init = false; /* 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 */ $mp4->boxes( [ 'moov' => [ 'trak' => [ 'mdia' => [ 'mdhd' => function ( $box ) use ( &$timescale ) { $version = $box->read8(); $flags = $box->read24(); if ( $version == 1 ) { $box->read64(); $box->read64(); } else { $box->read32(); $box->read32(); } $timescale = $box->read32(); } ], ], ], 'moof' => function ( $box ) use ( &$segments, &$moof, &$init, &$timestamp, &$duration, &$dts, &$first_pts, &$max_pts, &$timescale ) { if ( !$init ) { $init = [ 'start' => 0, 'size' => $box->start, 'timestamp' => 0.0, 'duration' => 0.0, ]; $segments['init'] = $init; } $moof = $box->start; $default_sample_duration = 0; $first_pts = 0; $max_pts = 0; $box->boxes( [ 'traf' => [ 'tfhd' => function( $box ) use ( &$default_sample_duration ) { $version = $box->read8(); $flags = $box->read24(); $track_id = $box->read32(); if ( $flags & MOV_TFHD_BASE_DATA_OFFSET ) { $box->read64(); } if ( $flags & MOV_TFHD_STSD_ID ) { $box->read32(); } if ( $flags & MOV_TFHD_DEFAULT_DURATION ) { $default_sample_duration = $box->read32(); } }, 'trun' => function( $box ) use ( &$default_sample_duration, &$dts, &$first_pts, &$max_pts ) { $version = $box->read8(); $flags = $box->read24(); $entries = $box->read32(); if ( $flags & MOV_TRUN_DATA_OFFSET ) { $box->read32(); } if ( $flags & MOV_TRUN_FIRST_SAMPLE_FLAGS ) { $box->read32(); } for ( $i = 0; $i < $entries; $i++ ) { $pts = $dts; $sample_duration = $default_sample_duration; if ( $flags & MOV_TRUN_SAMPLE_DURATION ) { $sample_duration = $box->read32(); } if ( $flags & MOV_TRUN_SAMPLE_SIZE ) { $box->read32(); } if ( $flags & MOV_TRUN_SAMPLE_FLAGS ) { $box->read32(); } if ( $flags & MOV_TRUN_SAMPLE_CTS ) { $pts += $box->read32(); } if ( $i == 0 ) { $first_pts = $pts; } $max_pts = max( $max_pts, $pts + $sample_duration ); $dts += $sample_duration; } echo "$first_pts - $max_pts\n"; } ], ] ); }, 'mdat' => function ( $box ) use ( &$segments, &$moof, &$first_pts, &$max_pts, &$timescale ) { var_dump( $first_pts ); var_dump( $max_pts ); var_dump( $timescale ); array_push( $segments, [ 'start' => $moof, 'size' => $box->end() - $moof, 'timestamp' => $first_pts / $timescale, 'duration' => ( $max_pts - $first_pts ) / $timescale, ] ); } ] ); 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"; } }