This commit is contained in:
Brooke Vibber 2023-03-02 13:11:27 -08:00
parent 5f942ee917
commit 6669b63bd2

View file

@ -1,27 +1,29 @@
<?php <?php
define( 'MOV_TFHD_BASE_DATA_OFFSET', 0x01 );
define( 'MOV_TFHD_STSD_ID', 0x02 );
define( 'MOV_TFHD_DEFAULT_DURATION', 0x08 );
define( 'MOV_TFHD_DEFAULT_SIZE', 0x10 );
define( 'MOV_TFHD_DEFAULT_FLAGS', 0x20 );
define( 'MOV_TFHD_DURATION_IS_EMPTY', 0x010000 );
define( 'MOV_TFHD_DEFAULT_BASE_IS_MOOF', 0x020000 );
define( 'MOV_TRUN_DATA_OFFSET', 0x01 );
define( 'MOV_TRUN_FIRST_SAMPLE_FLAGS', 0x04 );
define( 'MOV_TRUN_SAMPLE_DURATION', 0x100 );
define( 'MOV_TRUN_SAMPLE_SIZE', 0x200 );
define( 'MOV_TRUN_SAMPLE_FLAGS', 0x400 );
define( 'MOV_TRUN_SAMPLE_CTS', 0x800 );
class MP4Reader { class MP4Reader {
/** /**
* @var resource $file * @var resource $file
*/ */
private $file; protected $file;
/** public function __construct( $file ) {
* @param string $filename $this->file = $file;
*/
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;
}
} }
/** /**
@ -61,18 +63,65 @@ class MP4Reader {
} }
} }
/**
* @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 * @return int|false 32-bit integer value or false on eof
*/ */
public function read32() { public function read32() {
$bytes = $this->readBytes( 4 ); $bytes = $this->readBytes( 4 );
if ( $bytes === false ) { if ( $bytes === false ) {
return $bytes; return false;
} }
$data = unpack( 'Nval', $bytes ); $data = unpack( 'Nval', $bytes );
return $data['val']; 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 * @return string|false 4-byte type code or false on eof
*/ */
@ -82,7 +131,7 @@ class MP4Reader {
/** /**
* @param callable $callback in the form function(MP4Box) * @param callable $callback in the form function(MP4Box)
* @return mixed the return value from the callback, or false on eof * @return bool true on success, or false on eof
*/ */
public function readBox( $callback ) { public function readBox( $callback ) {
$start = $this->pos(); $start = $this->pos();
@ -97,35 +146,72 @@ class MP4Reader {
return false; return false;
} }
$box = new MP4Box( $this, $start, $size, $type ); $box = new MP4Box( $this->file, $start, $size, $type );
$retval = call_user_func( $callback, $box ); $retval = call_user_func( $callback, $box );
$remaining = $end - $this->pos(); $remaining = $end - $this->pos();
if ( $remaining > 0 ) { if ( $remaining > 0 ) {
$this->skipBytes( $remaining ); $this->skipBytes( $remaining );
} }
return $retval; 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 MP4Box { class MP4FileReader extends MP4Reader {
private $reader; /**
* @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 $start;
public $size; public $size;
public $type; public $type;
public function __construct( MP4Reader $reader, $start, $size, $type ) { public function __construct( $file, $start, $size, $type ) {
$this->reader = $reader; parent::__construct( $file );
$this->start = $start; $this->start = $start;
$this->size = $size; $this->size = $size;
$this->type = $type; $this->type = $type;
} }
public function pos() {
return $this->reader->pos();
}
public function end() { public function end() {
return $this->start + $this->size; return $this->start + $this->size;
} }
@ -134,32 +220,13 @@ class MP4Box {
return $this->end() - $this->pos(); 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 ) { public function readBytes( $length ) {
$this->guard( $length ); if ( $length > $this->remaining() ) {
return $this->reader->readBytes( $length ); return false;
}
return parent::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 ) { function hexdump( $str ) {
@ -197,66 +264,136 @@ function safestr( $str ) {
function extractFragmentedMP4( $filename ) { function extractFragmentedMP4( $filename ) {
$segments = []; $segments = [];
$mp4 = new MP4Reader( $filename ); $mp4 = new MP4FileReader( $filename );
$eof = false; $eof = false;
$moof = false; $moof = false;
$timestamp = 0.0;
$duration = 0.0;
$timescale = 0;
$dts = 0;
$first_pts = 0;
$max_pts = 0;
$init = 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
Need to: - https://www.w3.org/TR/mse-byte-stream-format-isobmff/#iso-init-segments
- find the end of the moov; everything up to that is the initialization segment - find the start of each styp+moof fragment
- https://www.w3.org/TR/mse-byte-stream-format-isobmff/#iso-init-segments - https://www.w3.org/TR/mse-byte-stream-format-isobmff/#iso-media-segments
- find the start of each styp+moof fragment - find the start timestamp of each moof fragment
- https://www.w3.org/TR/mse-byte-stream-format-isobmff/#iso-media-segments - find the duration of each moof fragment
- 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.timescale - looks useful 12288 for 24fps?
moov.trak.mdia.mdhd.duration - 0 on the moov moov.trak.mdia.mdhd.duration - 0 on the moov
moov.mvex.trex.default_sample_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.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.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_duration is empty here (uses default I guess?)
moof.mdat.sample_composition_time_offset also empty moof.mdat.sample_composition_time_offset also empty
opus has timescale 48000 in moov.trak.mdia.mdhd opus has timescale 48000 in moov.trak.mdia.mdhd
*/ */
$mp4->boxes( [
switch ( $box->type ) { 'moov' => [
case 'ftyp': 'trak' => [
break; 'mdia' => [
case 'moof': 'mdhd' => function ( $box ) use ( &$timescale ) {
if ( !$init ) { $version = $box->read8();
$init = [ $flags = $box->read24();
'start' => 0, if ( $version == 1 ) {
'size' => $box->end(), $box->read64();
'timestamp' => 0.0, $box->read64();
'duration' => 0.0, } else {
]; $box->read32();
$segments['init'] = $init; $box->read32();
}
$timescale = $box->read32();
} }
$moof = $box->start; ],
break; ],
case 'mdat': ],
// @todo use timestamp and duration data 'moof' => function ( $box ) use ( &$segments, &$moof, &$init, &$timestamp, &$duration, &$dts, &$first_pts, &$max_pts, &$timescale ) {
array_push( $segments, [ if ( !$init ) {
'start' => $moof, $init = [
'size' => $box->end() - $moof, 'start' => 0,
'timestamp' => 0.0, 'size' => $box->start,
'duration' => 0.0, 'timestamp' => 0.0,
] ); 'duration' => 0.0,
break; ];
default: $segments['init'] = $init;
// ignore
} }
$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();
return true; $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; return $segments;
} }