276 lines
7.3 KiB
PHP
276 lines
7.3 KiB
PHP
<?php
|
|
|
|
class MP4Reader {
|
|
|
|
/**
|
|
* @var resource $file
|
|
*/
|
|
private $file;
|
|
|
|
/**
|
|
* @param string $filename
|
|
*/
|
|
public function __construct( $filename ) {
|
|
$this->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";
|
|
}
|
|
}
|