diff --git a/extract-playlist.php b/extract-playlist.php new file mode 100644 index 0000000..982a64e --- /dev/null +++ b/extract-playlist.php @@ -0,0 +1,264 @@ +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 ); diff --git a/make-fmp4.sh b/make-fmp4.sh new file mode 100755 index 0000000..07cfcb7 --- /dev/null +++ b/make-fmp4.sh @@ -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