248 lines
6.7 KiB
PHP
248 lines
6.7 KiB
PHP
<?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
|
|
$class = $class_prefix.$header['name'];
|
|
|
|
$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;
|
|
}
|
|
} |