hls-test/extract-playlist.php
2023-03-01 13:55:30 -08:00

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";
}
}