WIP playlist extraction for fmp4
This commit is contained in:
parent
b14806a74b
commit
99c57170da
2 changed files with 301 additions and 0 deletions
264
extract-playlist.php
Normal file
264
extract-playlist.php
Normal file
|
@ -0,0 +1,264 @@
|
||||||
|
<?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;
|
||||||
|
|
||||||
|
function __construct( MP4Reader $reader, $start, $size, $type ) {
|
||||||
|
$this->reader = $reader;
|
||||||
|
$this->start = $start;
|
||||||
|
$this->size = $size;
|
||||||
|
$this->type = $type;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function pos() {
|
||||||
|
return $this->reader->pos();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function end() {
|
||||||
|
return $this->start + $this->size;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function remaining() {
|
||||||
|
return $this->end() - $this->pos();
|
||||||
|
}
|
||||||
|
|
||||||
|
private 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;
|
||||||
|
$init = false;
|
||||||
|
|
||||||
|
while ( !$eof ) {
|
||||||
|
$eof = !$mp4->readBox( function ( $box ) use ( &$segments, &$init ) {
|
||||||
|
$bytes = $box->readBytes( $box->size - 8 );
|
||||||
|
$bytes = substr( $bytes, 0, 16 );
|
||||||
|
$hex = hexdump( $bytes );
|
||||||
|
$safe = safestr( $bytes );
|
||||||
|
print "box: {$box->type} at {$box->start}, {$box->size} bytes {$hex} {$safe}\n";
|
||||||
|
|
||||||
|
/*
|
||||||
|
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
|
||||||
|
|
||||||
|
WARNING: opus is not dividing up fragments
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
if ( $box->type === 'moof' ) {
|
||||||
|
if ( !$init ) {
|
||||||
|
$init = [
|
||||||
|
'start' => 0,
|
||||||
|
'size' => $box->start + $box->size,
|
||||||
|
];
|
||||||
|
$segments['init'] = $init;
|
||||||
|
}
|
||||||
|
array_push( $segments, [
|
||||||
|
'start' => $box->start,
|
||||||
|
'size' => $box->size,
|
||||||
|
'timestamp' => 0.0,
|
||||||
|
'duration' => 0.0,
|
||||||
|
] );
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} );
|
||||||
|
}
|
||||||
|
|
||||||
|
return $segments;
|
||||||
|
}
|
||||||
|
|
||||||
|
$argv = $_SERVER['argv'];
|
||||||
|
$self = array_shift( $argv );
|
||||||
|
$filename = array_shift( $argv );
|
||||||
|
$segments = extractFragmentedMP4( $filename );
|
||||||
|
var_dump( $segments );
|
37
make-fmp4.sh
Executable file
37
make-fmp4.sh
Executable file
|
@ -0,0 +1,37 @@
|
||||||
|
interval=10
|
||||||
|
MOVFLAGS="-movflags +frag_keyframe+empty_moov -force_key_frames expr:gte(t,n_forced*$interval)"
|
||||||
|
AUDFLAGS="-movflags +empty_moov -frag_duration ${interval}000000"
|
||||||
|
BITRATE_HI="-b:v 1250k"
|
||||||
|
BITRATE_LO="-b:v 1000k"
|
||||||
|
|
||||||
|
SIZE_MAIN="-s 854x480"
|
||||||
|
|
||||||
|
VIDEO_H264="-vcodec h264 -g 240 $BITRATE_HI $SIZE_MAIN"
|
||||||
|
VIDEO_VP9="-vcodec libvpx-vp9 -tile-columns 2 -row-mt 1 -cpu-used 3 -g 240 $BITRATE_LO $SIZE_MAIN"
|
||||||
|
|
||||||
|
AUDIO_OPUS="-acodec libopus -ac 2 -ar 48000 -ab 96k"
|
||||||
|
AUDIO_AAC="-ac 2 -ar 44100 -ab 128k"
|
||||||
|
AUDIO_MP3="-acodec libmp3lame -ac 2 -ar 44100 -ab 128k"
|
||||||
|
|
||||||
|
INFILE=caminandes-llamigos.webm
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# Audio for HLS
|
||||||
|
ffmpeg -i $INFILE -vn $AUDIO_MP3 -y fmp4.audio.mp3
|
||||||
|
ffmpeg -i $INFILE -vn $AUDIO_AAC $AUDFLAGS -y fmp4.audio.aac.mp4
|
||||||
|
ffmpeg -i $INFILE -vn $AUDIO_OPUS $AUDFLAGS -y fmp4.audio.opus.mp4
|
||||||
|
|
||||||
|
# Video for HLS
|
||||||
|
ffmpeg -i $INFILE -an $VIDEO_H264 $MOVFLAGS -pass 1 -y fmp4.video.h264.mp4
|
||||||
|
ffmpeg -i $INFILE -an $VIDEO_H264 $MOVFLAGS -pass 2 -y fmp4.video.h264.mp4
|
||||||
|
|
||||||
|
ffmpeg -i $INFILE -an $VIDEO_VP9 $MOVFLAGS -pass 1 -y fmp4.video.vp9.mp4
|
||||||
|
ffmpeg -i $INFILE -an $VIDEO_VP9 $MOVFLAGS -pass 2 -y fmp4.video.vp9.mp4
|
||||||
|
|
||||||
|
# Playlist processing
|
||||||
|
#php extract-playlist.php fmp4.audio.mp3 fmp4.audio.mp3.m3u8
|
||||||
|
#php extract-playlist.php fmp4.audio.aac.mp4 fmp4.audio.aac.mp4.m3u8
|
||||||
|
#php extract-playlist.php fmp4.audio.opus.mp4 fmp4.audio.opus.mp4.m3u8
|
||||||
|
#php extract-playlist.php fmp4.video.h264.mp4 fmp4.video.h264.mp4.m3u8
|
||||||
|
#php extract-playlist.php fmp4.video.vp9.mp4 fmp4.video.vp9.mp4.m3u8
|
Loading…
Reference in a new issue