2024-09-24 11:55:40 +00:00
< ? php
namespace App\Media ;
use Illuminate\Support\Collection ;
use App\Media\MSVideo\Container ;
use App\Media\MSVideo\Containers\rlist\ { avih , isft , movi , strh };
use App\Media\MSVideo\Containers\Unknown ;
// https://www.jmcgowan.com/avi.html
class MSVideo extends Base
{
private const LOGKEY = 'MFM' ;
protected string $debug ;
private const container_classes = 'App\\Media\\MSVideo\\Containers\\' ;
public function __construct ( string $filename , string $type )
{
parent :: __construct ( $filename , $type );
$this -> size = $this -> filesize ;
$this -> offset = 0 ;
$this -> containers = collect ();
$this -> fopen ();
$format = $this -> fread ( 4 );
switch ( $format ) {
case 'RIFF' : // AVI, WAV, etc
case 'SDSS' : // SDSS is identical to RIFF, just renamed. Used by SmartSound QuickTracks (www.smartsound.com)
case 'RMP3' : // RMP3 is identical to RIFF, just renamed. Used by [unknown program] when creating RIFF-MP3s
$be = FALSE ; // Big Endian ints
$data = unpack ( 'Vsize/a4subtype' , $this -> fread ( 8 ));
// RMP3 is identical to WAVE, just renamed. Used by [unknown program] when creating RIFF-MP3s
if ( $data [ 'subtype' ] === 'RMP3' )
$data [ 'subtype' ] = 'WAVE' ;
// AMV files are RIFF-AVI files with parts of the spec deliberately broken, such as chunk size fields hardcoded to zero (because players known in hardware that these fields are always a certain size
if ( $data [ 'subtype' ] !== 'AMV ' ) {
// Handled separately in ParseRIFFAMV()
$this -> containers = $this -> get_containers ( self :: container_classes , Unknown :: class , $x = $this -> ftell (), $this -> filesize - $x , $be );
}
break ;
default :
$data = unpack ( 'Nsize/a4subtype' , $this -> fread ( 8 ));
dump ( $data );
throw new \Exception ( 'Cannot handle this RIFF file format yet: ' . $format );
}
$this -> fclose ();
}
public function __get ( string $key ) : mixed
{
switch ( $key ) {
case 'audio_channels' :
return $this -> getAudioAtoms ()
-> count ();
case 'audio_codec' :
case 'audio_samplerate' :
return $this -> getAudioAtoms ()
-> map ( fn ( $item ) => $item -> { $key })
-> join ( ',' );
// Signatures are calculated by the sha of the MDAT atom.
case 'signature' :
$container = $this -> find_containers ( movi :: class , 1 );
return $container ? -> { $key };
// Creation Time is in the MOOV/MVHD atom
case 'creation_date' :
return NULL ; // I dont think create date is in an AVI file
case 'duration' :
return $this -> getVideoAtoms ()
-> map ( fn ( $item ) => $item -> { $key })
-> join ( ',' );
// Height/Width is in the rlist/avih container
case 'height' :
case 'width' :
$container = $this -> find_containers ( avih :: class , 1 );
return $container ? -> { $key };
case 'gps_altitude' :
case 'gps_lat' :
case 'gps_lon' :
return NULL ; // No GPFS details in an AVI file?
case 'make' :
case 'model' :
return NULL ; // Make/Model of camera not in an avi file
case 'software' :
$container = $this -> find_containers ( isft :: class , 1 );
return $container ? -> software ;
case 'time_scale' :
$container = $this -> find_containers ( avih :: class , 1 );
return $container ? -> time_scale ;
case 'type' :
return parent :: __get ( $key );
case 'video_codec' :
case 'video_framerate' :
return $this -> getVideoAtoms ()
-> map ( fn ( $item ) => $item -> { $key })
-> join ( ',' );
default :
throw new \Exception ( 'Unknown key: ' . $key );
}
}
protected function get_containers ( string $class_prefix , string $unknown , int $offset , int $size , bool $be = TRUE , string $bytes = NULL , string $passthru = NULL , \Closure $callback = NULL ) : Collection
{
$rp = 0 ;
if ( ! $bytes ) {
$fh = fopen ( $this -> filename , 'r' );
fseek ( $fh , $offset );
}
$result = collect ();
while ( $rp < $size ) {
$read = $bytes ? substr ( $bytes , $rp , 8 ) : fread ( $fh , 8 );
$rp += 8 ;
$header = unpack ( sprintf ( 'a4name/%ssize' ,( $be ? 'N' : 'V' )), $read );
if ( strlen ( $header [ 'name' ] < 4 ))
throw new \Exception ( sprintf ( 'Name is less than 4 chars: [%s]' , $header [ 'name' ]));
if (( $header [ 'size' ] === 0 ) && ( $header [ 'name' ] !== 'JUNK' ))
throw new \Exception ( sprintf ( 'Chunk [%s] is unexpectedly zero' , $header [ 'name' ]));
// all structures are packed on word boundaries
if (( $header [ 'size' ] % 2 ) != 0 )
$header [ 'size' ] ++ ;
// We cant have a php function named 'list', so we change it to rlist
if ( $header [ 'name' ] === 'LIST' )
$header [ 'name' ] = 'RLIST' ;
// Load our class for this supplier
2024-09-27 11:29:18 +00:00
$class = $class_prefix . strtolower ( $header [ 'name' ]);
2024-09-24 11:55:40 +00:00
$data = $bytes
? substr ( $bytes , $rp , $header [ 'size' ])
: ( $header [ 'size' ] && ( $header [ 'size' ] <= self :: record_size ) ? fread ( $fh , $header [ 'size' ]) : NULL );
if ( $header [ 'size' ] >= 8 ) {
$o = class_exists ( $class )
? new $class ( $offset + $rp , $header [ 'size' ], $this -> filename , $be , $data , $passthru )
: new $unknown ( $offset + $rp , $header [ 'size' ], $this -> filename , $header [ 'name' ], $be , $data );
$result -> push ( $o );
$rp += $header [ 'size' ];
// Only need to seek if we didnt read all the data
if (( ! $bytes ) && ( $header [ 'size' ] > self :: record_size ))
fseek ( $fh , $offset + $rp );
} else {
dd ([ get_class ( $this ) => $data ]);
}
// Work out if data from the last container next to be passed onto the next one
if ( $callback )
$passthru = $callback ( $o );
}
if ( ! $bytes ) {
fclose ( $fh );
unset ( $fh );
}
return $result ;
}
/**
* Find all the video track atoms
*
* @ return Collection
* @ throws \Exception
*/
public function getAudioAtoms () : Collection
{
return $this -> find_containers ( strh :: class )
-> filter ( fn ( $item ) => $item -> type === 'auds' );
}
/**
* Find all the video track atoms
*
* @ return Collection
* @ throws \Exception
*/
public function getVideoAtoms () : Collection
{
return $this -> find_containers ( strh :: class )
-> filter ( fn ( $item ) => $item -> type === 'vids' );
}
/**
* Recursively look through our object hierarchy of containers looking for a specific one
*
* @ param string $subcontainer
* @ param int | NULL $expect
* @ param int $depth
* @ return Collection | Container | NULL
* @ throws \Exception
*/
protected function find_containers ( string $subcontainer , ? int $expect = NULL , int $depth = 100 ) : Collection | Container | NULL
{
if ( ! isset ( $this -> containers ) || ( $depth < 0 ))
return NULL ;
$subcontainero = $this -> containers -> filter ( fn ( $item ) => get_class ( $item ) === $subcontainer );
$subcontainero = $subcontainero
-> merge ( $this -> containers -> map ( fn ( $item ) => $item -> find_containers ( $subcontainer , NULL , $depth - 1 ))
-> filter ( fn ( $item ) => $item ? $item -> count () : NULL )
-> flatten ());
if ( ! $subcontainero -> count ())
return $subcontainero ;
if ( $expect && ( $subcontainero -> count () !== $expect ))
throw new \Exception ( sprintf ( '! Expected %d sub containers of %s, but have %d' , $expect , $subcontainer , $subcontainero -> count ()));
return ( $expect === 1 ) ? $subcontainero -> pop () : $subcontainero ;
}
}