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.strtolower($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; } }