WIP
This commit is contained in:
parent
5f942ee917
commit
6669b63bd2
1 changed files with 237 additions and 100 deletions
|
@ -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,13 +264,17 @@ 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:
|
Need to:
|
||||||
- find the end of the moov; everything up to that is the initialization segment
|
- find the end of the moov; everything up to that is the initialization segment
|
||||||
|
@ -225,38 +296,104 @@ function extractFragmentedMP4( $filename ) {
|
||||||
|
|
||||||
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 ) {
|
||||||
|
$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 ) {
|
if ( !$init ) {
|
||||||
$init = [
|
$init = [
|
||||||
'start' => 0,
|
'start' => 0,
|
||||||
'size' => $box->end(),
|
'size' => $box->start,
|
||||||
'timestamp' => 0.0,
|
'timestamp' => 0.0,
|
||||||
'duration' => 0.0,
|
'duration' => 0.0,
|
||||||
];
|
];
|
||||||
$segments['init'] = $init;
|
$segments['init'] = $init;
|
||||||
}
|
}
|
||||||
$moof = $box->start;
|
$moof = $box->start;
|
||||||
break;
|
$default_sample_duration = 0;
|
||||||
case 'mdat':
|
$first_pts = 0;
|
||||||
// @todo use timestamp and duration data
|
$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, [
|
array_push( $segments, [
|
||||||
'start' => $moof,
|
'start' => $moof,
|
||||||
'size' => $box->end() - $moof,
|
'size' => $box->end() - $moof,
|
||||||
'timestamp' => 0.0,
|
'timestamp' => $first_pts / $timescale,
|
||||||
'duration' => 0.0,
|
'duration' => ( $max_pts - $first_pts ) / $timescale,
|
||||||
] );
|
] );
|
||||||
break;
|
|
||||||
default:
|
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
} );
|
|
||||||
}
|
}
|
||||||
|
] );
|
||||||
|
|
||||||
return $segments;
|
return $segments;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue