From f9cdc8f9d2674563d3acf09ad99fdfe7d19e541a Mon Sep 17 00:00:00 2001 From: Deon George Date: Mon, 16 Sep 2024 22:10:19 +1000 Subject: [PATCH] Implement our own quicktime parser --- app/Console/Commands/CatalogScan.php | 2 +- app/Http/Controllers/VideoController.php | 5 - app/Jobs/CatalogScan.php | 10 +- app/Media/Base.php | 42 +++++ app/Media/Factory.php | 30 ++++ app/Media/QuickTime.php | 121 +++++++++++++++ app/Media/QuickTime/Atom.php | 144 ++++++++++++++++++ app/Media/QuickTime/Atoms/SubAtom.php | 59 +++++++ app/Media/QuickTime/Atoms/Unknown.php | 23 +++ app/Media/QuickTime/Atoms/free.php | 12 ++ app/Media/QuickTime/Atoms/ftyp.php | 34 +++++ app/Media/QuickTime/Atoms/mdat.php | 42 +++++ app/Media/QuickTime/Atoms/moov.php | 19 +++ app/Media/QuickTime/Atoms/moov/free.php | 12 ++ app/Media/QuickTime/Atoms/moov/meta.php | 47 ++++++ app/Media/QuickTime/Atoms/moov/meta/free.php | 12 ++ app/Media/QuickTime/Atoms/moov/meta/ilst.php | 41 +++++ app/Media/QuickTime/Atoms/moov/meta/keys.php | 26 ++++ app/Media/QuickTime/Atoms/moov/mvhd.php | 56 +++++++ app/Media/QuickTime/Atoms/moov/trak.php | 19 +++ app/Media/QuickTime/Atoms/moov/trak/mdia.php | 33 ++++ .../QuickTime/Atoms/moov/trak/mdia/hdlr.php | 32 ++++ .../QuickTime/Atoms/moov/trak/mdia/minf.php | 39 +++++ .../Atoms/moov/trak/mdia/minf/hdlr.php | 10 ++ .../Atoms/moov/trak/mdia/minf/stbl.php | 22 +++ .../Atoms/moov/trak/mdia/minf/stbl/stsd.php | 104 +++++++++++++ .../Atoms/moov/trak/mdia/minf/stbl/stts.php | 32 ++++ app/Media/QuickTime/Atoms/moov/trak/meta.php | 10 ++ app/Media/QuickTime/Atoms/moov/trak/tkhd.php | 55 +++++++ app/Media/QuickTime/Atoms/skip.php | 12 ++ app/Media/QuickTime/Atoms/wide.php | 13 ++ app/Media/Unknown.php | 24 +++ app/Models/Photo.php | 13 +- app/Traits/FindQuicktimeAtoms.php | 104 +++++++++++++ composer.json | 2 +- helpers.php | 92 ++++++++++- resources/views/components/map.blade.php | 19 +++ .../components/photo/thumbnail.blade.php | 3 + .../views/components/thumbnail.blade.php | 3 - resources/views/photo/view.blade.php | 45 +----- .../views/photo/widgets/thumbnail.blade.php | 2 +- routes/web.php | 4 +- 42 files changed, 1367 insertions(+), 62 deletions(-) create mode 100644 app/Media/Base.php create mode 100644 app/Media/Factory.php create mode 100644 app/Media/QuickTime.php create mode 100644 app/Media/QuickTime/Atom.php create mode 100644 app/Media/QuickTime/Atoms/SubAtom.php create mode 100644 app/Media/QuickTime/Atoms/Unknown.php create mode 100644 app/Media/QuickTime/Atoms/free.php create mode 100644 app/Media/QuickTime/Atoms/ftyp.php create mode 100644 app/Media/QuickTime/Atoms/mdat.php create mode 100644 app/Media/QuickTime/Atoms/moov.php create mode 100644 app/Media/QuickTime/Atoms/moov/free.php create mode 100644 app/Media/QuickTime/Atoms/moov/meta.php create mode 100644 app/Media/QuickTime/Atoms/moov/meta/free.php create mode 100644 app/Media/QuickTime/Atoms/moov/meta/ilst.php create mode 100644 app/Media/QuickTime/Atoms/moov/meta/keys.php create mode 100644 app/Media/QuickTime/Atoms/moov/mvhd.php create mode 100644 app/Media/QuickTime/Atoms/moov/trak.php create mode 100644 app/Media/QuickTime/Atoms/moov/trak/mdia.php create mode 100644 app/Media/QuickTime/Atoms/moov/trak/mdia/hdlr.php create mode 100644 app/Media/QuickTime/Atoms/moov/trak/mdia/minf.php create mode 100644 app/Media/QuickTime/Atoms/moov/trak/mdia/minf/hdlr.php create mode 100644 app/Media/QuickTime/Atoms/moov/trak/mdia/minf/stbl.php create mode 100644 app/Media/QuickTime/Atoms/moov/trak/mdia/minf/stbl/stsd.php create mode 100644 app/Media/QuickTime/Atoms/moov/trak/mdia/minf/stbl/stts.php create mode 100644 app/Media/QuickTime/Atoms/moov/trak/meta.php create mode 100644 app/Media/QuickTime/Atoms/moov/trak/tkhd.php create mode 100644 app/Media/QuickTime/Atoms/skip.php create mode 100644 app/Media/QuickTime/Atoms/wide.php create mode 100644 app/Media/Unknown.php create mode 100644 app/Traits/FindQuicktimeAtoms.php create mode 100644 resources/views/components/map.blade.php create mode 100644 resources/views/components/photo/thumbnail.blade.php delete mode 100644 resources/views/components/thumbnail.blade.php diff --git a/app/Console/Commands/CatalogScan.php b/app/Console/Commands/CatalogScan.php index 6f85d79..e69db24 100644 --- a/app/Console/Commands/CatalogScan.php +++ b/app/Console/Commands/CatalogScan.php @@ -39,6 +39,6 @@ class CatalogScan extends Command $o = $class::findOrFail($this->argument('id')); - return Job::dispatchSync($o); + return Job::dispatchSync($o,$this->option('dirty')); } } \ No newline at end of file diff --git a/app/Http/Controllers/VideoController.php b/app/Http/Controllers/VideoController.php index b395162..bd40ccb 100644 --- a/app/Http/Controllers/VideoController.php +++ b/app/Http/Controllers/VideoController.php @@ -39,11 +39,6 @@ class VideoController extends Controller ]); } - public function info(Video $o) - { - return view('video.view',['o'=>$o]); - } - public function undelete(Video $o) { $o->remove = NULL; diff --git a/app/Jobs/CatalogScan.php b/app/Jobs/CatalogScan.php index 44c8f0e..50f51c5 100644 --- a/app/Jobs/CatalogScan.php +++ b/app/Jobs/CatalogScan.php @@ -91,10 +91,12 @@ class CatalogScan implements ShouldQueue, ShouldBeUnique } // If the file signature changed, abort the update. - if ($this->o->getOriginal('file_signature') && $this->o->wasChanged('file_signature')) { - dump(['old'=>$this->o->getOriginal('file_signature'),'new'=>$this->o->file_signature]); - abort(500,'File Signature Changed?'); - } + if ($this->o->getOriginal('file_signature') && $this->o->wasChanged('file_signature')) + throw new \Exception(sprintf('File Signature Changed for [%s] DB: [%s], File: [%s]?', + $this->o->file_name(), + $this->o->file_signature, + $this->o->getOriginal('file_signature'), + )); $this->o->save(); } diff --git a/app/Media/Base.php b/app/Media/Base.php new file mode 100644 index 0000000..d4f7cce --- /dev/null +++ b/app/Media/Base.php @@ -0,0 +1,42 @@ +filename = $filename; + $this->filesize = filesize($filename); + $this->type = $type; + } + + /** + * Enable getting values for keys in the response + * + * @param string $key + * @return mixed|object + * @throws \Exception + */ + public function __get(string $key): mixed + { + switch ($key) { + case 'type': + return $this->type; + + default: + throw new \Exception('Unknown key: '.$key); + } + } +} \ No newline at end of file diff --git a/app/Media/Factory.php b/app/Media/Factory.php new file mode 100644 index 0000000..2e45c26 --- /dev/null +++ b/app/Media/Factory.php @@ -0,0 +1,30 @@ + QuickTime::class, + ]; + + /** + * Returns new event instance + * + * @param string $file + * @return Base + */ + public static function create(string $file): Base + { + $type = mime_content_type($file); + $class = Arr::get(self::map,$type,Unknown::class); + + return new $class($file,$type); + } +} \ No newline at end of file diff --git a/app/Media/QuickTime.php b/app/Media/QuickTime.php new file mode 100644 index 0000000..f5fb177 --- /dev/null +++ b/app/Media/QuickTime.php @@ -0,0 +1,121 @@ +atoms = $this->get_atoms(self::atom_classes,Unknown::class,0,$this->filesize); + } + + public function __get(string $key): mixed + { + switch ($key) { + case 'audio_channels': + case 'audio_codec': + case 'audio_samplerate': + return $this->getAudioAtoms() + ->map(fn($item)=>$item->find_atoms(moov\trak\mdia\minf\stbl\stsd::class,1)) + ->flatten() + ->map(fn($item)=>$item->{$key}) + ->join(','); + + // Signatures are calculated by the sha of the MDAT atom. + case 'signature': + $atom = $this->find_atoms(mdat::class,1); + + return $atom->signature; + + // Creation Time is in the MOOV/MVHD atom + case 'creation_date': + case 'duration': + case 'preferred_rate': + case 'preferred_volume': + // Height/Width is in the moov/trak/tkhd attom + case 'height': + case 'width': + $atom = $this->find_atoms(moov::class,1); + + return $atom->{$key}; + + case 'gps_altitude': + case 'gps_lat': + case 'gps_lon': + $atom = $this->find_atoms(moov::class,1) + ->find_atoms(moov\meta::class,1); + + return $atom->{$key}; + + case 'time_scale': + $atom = $this->find_atoms(moov\mvhd::class,1); + + return $atom->{$key}; + + case 'type': + return parent::__get($key); + + case 'video_codec': + return $this->getVideoAtoms() + ->map(fn($item)=>$item->find_atoms(moov\trak\mdia\minf\stbl\stsd::class,1)) + ->flatten() + ->map(fn($item)=>$item->{$key}) + ->join(','); + + case 'video_framerate': + $atom = $this->getVideoAtoms() + ->map(fn($item)=>$item->find_atoms(moov\trak\mdia\minf\stbl\stts::class,1)) + ->pop(); + + return $atom->frame_rate($this->time_scale); + + default: + throw new \Exception('Unknown key: '.$key); + } + } + + /** + * Find all the audio track atoms + * + * Audio atoms are in moov/trak/mdia/minf + * + * @return Collection + * @throws \Exception + */ + public function getAudioAtoms(): Collection + { + return $this->find_atoms(moov\trak\mdia\minf::class) + ->filter(fn($item)=>$item->type==='soun'); + } + + /** + * Find all the video track atoms + * + * Audio atoms are in moov/trak/mdia/minf + * + * @return Collection + * @throws \Exception + */ + public function getVideoAtoms(): Collection + { + return $this->find_atoms(moov\trak\mdia\minf::class) + ->filter(fn($item)=>$item->type==='vide'); + } +} \ No newline at end of file diff --git a/app/Media/QuickTime/Atom.php b/app/Media/QuickTime/Atom.php new file mode 100644 index 0000000..ea6d169 --- /dev/null +++ b/app/Media/QuickTime/Atom.php @@ -0,0 +1,144 @@ +offset = $offset; + + // Quick validation + if ($size < 0) + throw new \Exception(sprintf('Atom cannot be negative. (%d)',$size)); + + $this->size = $size; + $this->filename = $filename; + $this->cache = collect(); + } + + public function __get(string $key): mixed + { + switch ($key) { + // Create time is in the MOOV/MVHD atom + case 'creation_date': + case 'duration': + case 'preferred_rate': + case 'preferred_volume': + $subatom = $this->find_atoms(mvhd::class,1); + + return $subatom->{$key}; + + // Height is in the moov/trak/tkhd attom + case 'height': + // Width is in the moov/trak/tkhd attom + case 'width': + $atom = $this->find_atoms(trak::class); + + return $atom->map(fn($item)=>$item->{$key})->filter()->max(); + + // Signatures are calculated by the sha of the MDAT atom. + case 'signature': + return $this->signature(); + + default: + throw new \Exception('Unknown key: '.$key); + } + } + + protected function data(): string + { + // Quick validation + if ($this->size > self::record_size) + throw new \Exception(sprintf('Refusing to read more than %d of data',self::record_size)); + + $data = ''; + if ($this->fopen()) { + while (! is_null($read=$this->fread())) + $data .= $read; + + $this->fclose(); + } + + return $data; + } + + protected function fclose(): bool + { + fclose($this->fh); + unset($this->fh); + + return TRUE; + } + + /** + * Open the file and seek to the atom + * + * @return bool + */ + protected function fopen(): bool + { + $this->fh = fopen($this->filename,'r'); + fseek($this->fh,$this->offset); + $this->fp = 0; + + return TRUE; + } + + /** + * Read the atom from the file + * + * @param int $size + * @return string|NULL + */ + protected function fread(int $size=4096): ?string + { + if ($this->fp === $this->size) + return NULL; + + if ($this->fp+$size > $this->size) + $size = $this->size-$this->fp; + + $read = fread($this->fh,$size); + $this->fp += $size; + + return $read; + } + + protected function fseek(int $offset): int + { + $this->fp = $offset; + + return fseek($this->fh,$this->offset+$this->fp); + } + + protected function unpack(array $unpack=[]): string + { + return collect($unpack ?: static::unpack)->map(fn($v,$k)=>$v[0].$k)->join('/'); + } + + protected function unpack_size(array $unpack=[]): int + { + return collect($unpack ?: static::unpack)->map(fn($v,$k)=>$v[1])->sum(); + } +} \ No newline at end of file diff --git a/app/Media/QuickTime/Atoms/SubAtom.php b/app/Media/QuickTime/Atoms/SubAtom.php new file mode 100644 index 0000000..d48c472 --- /dev/null +++ b/app/Media/QuickTime/Atoms/SubAtom.php @@ -0,0 +1,59 @@ +['c',1], + 'flags'=>['a3',3], + 'count'=>['N',4], + ]; + + public function __get(string $key): mixed + { + switch ($key) { + // Height is in the moov/trak/tkhd attom + case 'height': + // Width is in the moov/trak/tkhd attom + case 'width': + $atom = $this->find_atoms(tkhd::class,1); + + return $atom->{$key}; + + default: + throw new \Exception('Unknown key: '.$key); + } + } + + /** + * Unpack data into our cache + * + * @param string|null $data + * @return Collection + * @throws \Exception + */ + protected function cache(?string $data=NULL): Collection + { + $data = $data ?: $this->data(); + + if (! count($this->cache) && $this->size) { + $this->cache = collect(unpack($this->unpack(),$data)); + + if ($this->size > ($x=$this->unpack_size())) + $this->unused_data = substr($data,$x); + } + + return $this->cache; + } +} \ No newline at end of file diff --git a/app/Media/QuickTime/Atoms/Unknown.php b/app/Media/QuickTime/Atoms/Unknown.php new file mode 100644 index 0000000..5c318f6 --- /dev/null +++ b/app/Media/QuickTime/Atoms/Unknown.php @@ -0,0 +1,23 @@ +atom = $atom; + + // For debugging + if (FALSE) + $this->debug = hex_dump($data ?: $this->data()); + } +} \ No newline at end of file diff --git a/app/Media/QuickTime/Atoms/free.php b/app/Media/QuickTime/Atoms/free.php new file mode 100644 index 0000000..2424807 --- /dev/null +++ b/app/Media/QuickTime/Atoms/free.php @@ -0,0 +1,12 @@ +['a4',1], + 'minor'=>['a4',4], + 'compat'=>['a4',4], + ]; + + public function __construct(int $offset,int $size,string $filename,?string $data) { + if ($size > 12) + throw new \Exception('FTYP atom larger than 12 bytes, we wont be able to handled that'); + + parent::__construct($offset,$size,$filename); + + $this->cache['data'] = unpack($this->unpack(),$data); + + if (Arr::get($this->cache,'data.compat') !== 'qt ') + throw new \Exception('This is not a QT format file'); + } +} \ No newline at end of file diff --git a/app/Media/QuickTime/Atoms/mdat.php b/app/Media/QuickTime/Atoms/mdat.php new file mode 100644 index 0000000..1c32a9f --- /dev/null +++ b/app/Media/QuickTime/Atoms/mdat.php @@ -0,0 +1,42 @@ +cache,'signature')) { + if ($this->size) { + $this->fopen(); + + $hash = hash_init($alg); + + while (!is_null($read = $this->fread(16384))) + hash_update($hash, $read); + + $this->fclose(); + + $this->cache['signature'] = hash_final($hash); + + } else { + $this->cache['signature'] = NULL; + } + } + + return $this->cache['signature']; + } +} \ No newline at end of file diff --git a/app/Media/QuickTime/Atoms/moov.php b/app/Media/QuickTime/Atoms/moov.php new file mode 100644 index 0000000..667e324 --- /dev/null +++ b/app/Media/QuickTime/Atoms/moov.php @@ -0,0 +1,19 @@ +atoms = $this->get_atoms(self::subatom_classes,Unknown::class,$offset,$size,$data); + } +} \ No newline at end of file diff --git a/app/Media/QuickTime/Atoms/moov/free.php b/app/Media/QuickTime/Atoms/moov/free.php new file mode 100644 index 0000000..0b7e756 --- /dev/null +++ b/app/Media/QuickTime/Atoms/moov/free.php @@ -0,0 +1,12 @@ +atoms = $this->get_atoms(self::subatom_classes,Unknown::class,$offset,$size,$data); + + $keys = $this->find_atoms(keys::class,1); + $values = $this->find_atoms(ilst::class,1); + + $this->cache = $keys->cache->combine($values->cache); + } + + public function __get(string $key): mixed + { + switch ($key) { + case 'gps': + $m = []; + $gps = Arr::get($this->cache,'mdta.com.apple.quicktime.location.ISO6709'); + preg_match('/^([+-][0-9]{2,6}(?:\.[0-9]+)?)([+-][0-9]{3,7}(?:\.[0-9]+)?)([+-][0-9]+(?:\.[0-9]+)?)?/',$gps,$m); + + return ['lat'=>(float)$m[1],'lon'=>(float)$m[2],'alt'=>(float)$m[3]]; + + case 'gps_altitude': + return Arr::get($this->gps,'alt'); + case 'gps_lat': + return Arr::get($this->gps,'lat'); + case 'gps_lon': + return Arr::get($this->gps,'lon'); + + default: + return parent::__get($key); + } + } +} \ No newline at end of file diff --git a/app/Media/QuickTime/Atoms/moov/meta/free.php b/app/Media/QuickTime/Atoms/moov/meta/free.php new file mode 100644 index 0000000..cc6d291 --- /dev/null +++ b/app/Media/QuickTime/Atoms/moov/meta/free.php @@ -0,0 +1,12 @@ +cache = $this->cache->push($c['data']); + } + } +} \ No newline at end of file diff --git a/app/Media/QuickTime/Atoms/moov/meta/keys.php b/app/Media/QuickTime/Atoms/moov/meta/keys.php new file mode 100644 index 0000000..98a2b63 --- /dev/null +++ b/app/Media/QuickTime/Atoms/moov/meta/keys.php @@ -0,0 +1,26 @@ +keys = collect(); + $read = unpack($this->unpack(self::atom_record),substr($data,0,$ptr=$this->unpack_size(self::atom_record))); + + for ($i=0; $i<$read['count']; $i++) { + $key_size = unpack('Nsize',substr($data,$ptr,4)); + $keys = unpack(sprintf('a4namespace/a%dname',$key_size['size']-8),substr($data,$ptr+4,4+$key_size['size']-8)); + $ptr += $key_size['size']; + $this->cache = $this->cache->push(sprintf('%s.%s',$keys['namespace'],$keys['name'])); + } + } +} \ No newline at end of file diff --git a/app/Media/QuickTime/Atoms/moov/mvhd.php b/app/Media/QuickTime/Atoms/moov/mvhd.php new file mode 100644 index 0000000..3f553a1 --- /dev/null +++ b/app/Media/QuickTime/Atoms/moov/mvhd.php @@ -0,0 +1,56 @@ +['c',1], + 'flags'=>['a3',3], + 'create'=>['N',4], + 'modified'=>['N',4], + 'time_scale'=>['N',4], + 'duration'=>['N',4], + 'prate'=>['a4',4], + 'pvolume'=>['a2',2], + 'matrix'=>['a36',36], + 'prevtime'=>['N',4], + 'prevdur'=>['N',4], + 'postertime'=>['N',4], + 'selecttime'=>['N',4], + 'selectdur'=>['N',4], + 'curtime'=>['N',4], + 'nexttrack'=>['N',4], + ]; + + public function __get($key): mixed + { + switch ($key) { + case 'creation_date': + // We need to convert from MAC time 1904-01-01 to epoch + return Carbon::createFromTimestamp(Arr::get($this->cache(),'create')-2082844800); + + case 'duration': + return ($x=$this->time_scale) ? round(Arr::get($this->cache(),'duration')/$x,3) : NULL; + + case 'preferred_rate': + return fixed_point_int_to_float(Arr::get($this->cache(),'prate')); + + case 'preferred_volume': + return fixed_point_int_to_float(Arr::get($this->cache(),'pvolume')); + + case 'time_scale': + return round(Arr::get($this->cache(),$key)); + + default: + return parent::__get($key); + } + } +} \ No newline at end of file diff --git a/app/Media/QuickTime/Atoms/moov/trak.php b/app/Media/QuickTime/Atoms/moov/trak.php new file mode 100644 index 0000000..bfb9c8c --- /dev/null +++ b/app/Media/QuickTime/Atoms/moov/trak.php @@ -0,0 +1,19 @@ +atoms = $this->get_atoms(self::subatom_classes,Unknown::class,$offset,$size,$data); + } +} \ No newline at end of file diff --git a/app/Media/QuickTime/Atoms/moov/trak/mdia.php b/app/Media/QuickTime/Atoms/moov/trak/mdia.php new file mode 100644 index 0000000..e0ed19b --- /dev/null +++ b/app/Media/QuickTime/Atoms/moov/trak/mdia.php @@ -0,0 +1,33 @@ +atoms = $this->get_atoms( + self::subatom_classes, + Unknown::class, + $offset, + $size, + $data, + NULL, + fn($atom)=>($atom instanceof hdlr) ? $atom->cache['csubtype'] : NULL + ); + + // For debugging + if (FALSE) + $this->debug = hex_dump($data ?: $this->data()); + } +} \ No newline at end of file diff --git a/app/Media/QuickTime/Atoms/moov/trak/mdia/hdlr.php b/app/Media/QuickTime/Atoms/moov/trak/mdia/hdlr.php new file mode 100644 index 0000000..6904f70 --- /dev/null +++ b/app/Media/QuickTime/Atoms/moov/trak/mdia/hdlr.php @@ -0,0 +1,32 @@ +['c',1], + 'flags'=>['a3',3], + 'ctype'=>['a4',4], + 'csubtype'=>['a4',4], + 'cmanufact'=>['a4',4], + 'cflags'=>['a4',4], + 'cmask'=>['a4',4], + ]; + + public function __construct(int $offset,int $size,string $filename,?string $data) + { + parent::__construct($offset,$size,$filename); + + $this->cache = $this->cache(); + $this->cache['name'] = pascal_string($this->unused_data); + + $this->unused_data = (($x=strlen($this->cache['name'])+1) < strlen($this->unused_data)) ? substr($data,$x) : NULL; + + // For debugging + if (FALSE) + $this->debug = hex_dump($data ?: $this->data()); + } +} \ No newline at end of file diff --git a/app/Media/QuickTime/Atoms/moov/trak/mdia/minf.php b/app/Media/QuickTime/Atoms/moov/trak/mdia/minf.php new file mode 100644 index 0000000..31bc672 --- /dev/null +++ b/app/Media/QuickTime/Atoms/moov/trak/mdia/minf.php @@ -0,0 +1,39 @@ +type = $arg; + + $this->atoms = $this->get_atoms(self::subatom_classes,Unknown::class,$offset,$size,$data,$arg); + + // For debugging + if (FALSE) + $this->debug = hex_dump($data ?: $this->data()); + } + + public function __get(string $key): mixed + { + switch ($key) { + case 'type': + return $this->{$key}; + + default: + return parent::__get($key); + } + } +} \ No newline at end of file diff --git a/app/Media/QuickTime/Atoms/moov/trak/mdia/minf/hdlr.php b/app/Media/QuickTime/Atoms/moov/trak/mdia/minf/hdlr.php new file mode 100644 index 0000000..89e0c83 --- /dev/null +++ b/app/Media/QuickTime/Atoms/moov/trak/mdia/minf/hdlr.php @@ -0,0 +1,10 @@ +atoms = $this->get_atoms(self::subatom_classes,Unknown::class,$offset,$size,$data,$arg); + + // For debugging + if (FALSE) + $this->debug = hex_dump($data ?: $this->data()); + } +} \ No newline at end of file diff --git a/app/Media/QuickTime/Atoms/moov/trak/mdia/minf/stbl/stsd.php b/app/Media/QuickTime/Atoms/moov/trak/mdia/minf/stbl/stsd.php new file mode 100644 index 0000000..6d053a5 --- /dev/null +++ b/app/Media/QuickTime/Atoms/moov/trak/mdia/minf/stbl/stsd.php @@ -0,0 +1,104 @@ +['N',4], + 'format'=>['a4',4], + 'reserved'=>['a6',6], + 'index'=>['n',2], + 'encoder_version'=>['n',2], + 'encoder_revision'=>['n',2], + 'encoder_vendor'=>['a4',4], + ]; + + protected const audio_record = [ + 'audio_channels'=>['n',2], + 'audio_bit_depth'=>['n',2], + 'audio_compression_id'=>['n',2], + 'audio_packet_size'=>['n',2], + 'audio_sample_rate'=>['a4',4], + ]; + + protected const video_record = [ + 'temporal_quality'=>['N',4], + 'spatial_quality'=>['N',4], + 'width'=>['n',2], + 'height'=>['n',2], + 'resolution_x'=>['a4',4], + 'resolution_y'=>['a4',4], + 'data_size'=>['N',4], + 'frame_count'=>['n',2], + //'codec_name'=>['a4',4], // pascal string + ]; + + public function __construct(int $offset,int $size,string $filename,?string $data,string $arg=NULL) + { + parent::__construct($offset,$size,$filename); + $this->type = $arg; + + $read = unpack($this->unpack(self::atom_record),substr($data,0,$ptr=$this->unpack_size(self::atom_record))); + + for ($i=0; $i<$read['count']; $i++) { + $this->cache = collect(unpack($this->unpack(),substr($data,$ptr,$x=$this->unpack_size()))); + + $ptr += $x; + + switch ($this->type) { + case 'soun': + // Audio Track + $this->audio = unpack($this->unpack(self::audio_record),substr($data,$ptr,$x=$this->unpack_size(self::audio_record))); + $ptr += $x; + + break; + + case 'vide': + // Video Track + $this->video = unpack($this->unpack(self::video_record),substr($data,$ptr,$x=$this->unpack_size(self::video_record))); + $ptr += $x; + + // codec - pascal string + $this->video['codec'] = pascal_string(substr($data,$ptr)); + + $ptr += strlen($this->video['codec'])+1; + } + + $this->extra = substr($data,$ptr); + } + } + + public function __get(string $key):mixed + { + switch ($key) { + case 'audio_channels': + return Arr::get($this->audio,$key); + + case 'audio_codec': + switch ($this->cache->get('format')) { + case 'mp4a': return 'ISO/IEC 14496-3 AAC'; + + default: + return $this->cache->get('format'); + } + + case 'audio_samplerate': + return fixed_point_int_to_float(Arr::get($this->audio,'audio_sample_rate',0)); + + case 'video_codec': + return Arr::get($this->video,'codec'); + + case 'video_framerate': + dd($this); + // return Arr::get($this->video,'codec'); + + default: + return parent::__get($key); + } + } +} \ No newline at end of file diff --git a/app/Media/QuickTime/Atoms/moov/trak/mdia/minf/stbl/stts.php b/app/Media/QuickTime/Atoms/moov/trak/mdia/minf/stbl/stts.php new file mode 100644 index 0000000..c179a81 --- /dev/null +++ b/app/Media/QuickTime/Atoms/moov/trak/mdia/minf/stbl/stts.php @@ -0,0 +1,32 @@ +unpack(self::atom_record),substr($data,0,$ptr=$this->unpack_size(self::atom_record))); + + for ($i=0; $i<$read['count']; $i++) { + $this->cache->push(unpack('Ncount/Nduration',substr($data,$ptr,8))); + $ptr += 8; + } + + // For debugging + if (FALSE) + $this->debug = hex_dump($data ?: $this->data()); + } + + public function frame_rate(float $time_scale): float + { + return $this->cache + ->pluck('duration') + ->map(fn($item)=>$time_scale/$item) + ->max(); + } +} \ No newline at end of file diff --git a/app/Media/QuickTime/Atoms/moov/trak/meta.php b/app/Media/QuickTime/Atoms/moov/trak/meta.php new file mode 100644 index 0000000..85eb1e9 --- /dev/null +++ b/app/Media/QuickTime/Atoms/moov/trak/meta.php @@ -0,0 +1,10 @@ +['c',1], + 'flags'=>['a3',3], + 'create'=>['N',4], + 'modified'=>['N',4], + 'trakid'=>['N',4], // The value 0 cannot be used + 'reserved1'=>['a4',4], + 'duration'=>['N',4], + 'reserved2'=>['a8',8], + 'layer'=>['n',2], + 'altgroup'=>['n',2], + 'volume'=>['a2',2], // 16 bit fixed point + 'reserved3'=>['a2',2], + 'matrix'=>['a36',36], + 'twidth'=>['a4',4], // 32 bit fixed point + 'theight'=>['a4',4], // 32 bit fixed point + ]; + + public function __construct(int $offset,int $size,string $filename,?string $data) + { + parent::__construct($offset,$size,$filename,$data); + + $this->cache = $this->cache(); + + // For debugging + if (FALSE) + $this->debug = hex_dump($data ?: $this->data()); + } + + public function __get($key): mixed + { + switch ($key) { + case 'height': + return fixed_point_int_to_float(Arr::get($this->cache(),'theight')); + + case 'width': + return fixed_point_int_to_float(Arr::get($this->cache(),'twidth')); + + default: + return parent::__get($key); + } + } +} \ No newline at end of file diff --git a/app/Media/QuickTime/Atoms/skip.php b/app/Media/QuickTime/Atoms/skip.php new file mode 100644 index 0000000..74582d7 --- /dev/null +++ b/app/Media/QuickTime/Atoms/skip.php @@ -0,0 +1,12 @@ +type = $type; + } + + public function __get(string $key): mixed + { + return NULL; + } +} \ No newline at end of file diff --git a/app/Models/Photo.php b/app/Models/Photo.php index 3c5c00c..a4bd42c 100644 --- a/app/Models/Photo.php +++ b/app/Models/Photo.php @@ -4,10 +4,10 @@ namespace App\Models; use Carbon\Carbon; use Illuminate\Support\Facades\Log; -use Illuminate\Support\Facades\Route; use Illuminate\Support\Traits\ForwardsCalls; use Imagick; +use App\Casts\PostgresBytea; use App\Jobs\CatalogMove; class Photo extends Abstracted\Catalog @@ -16,9 +16,14 @@ class Photo extends Abstracted\Catalog public const config = 'photo'; + protected $casts = [ + 'created'=>'datetime:Y-m-d H:i:s', + 'thumbnail'=>PostgresBytea::class, + ]; + protected static $includeSubSecTime = TRUE; - // Imagick Objectfile_name + // Imagick Object private ?Imagick $_o; protected array $init = [ 'creation_date', @@ -102,10 +107,10 @@ class Photo extends Abstracted\Catalog if (isset($this->_o)) return $this->_o; - if ((!file_exists($this->file_name(FALSE))) || (!is_readable($this->file_name(FALSE)))) + if ((! file_exists($this->file_name(FALSE))) || (! is_readable($this->file_name(FALSE)))) return $this->_o = NULL; - if (!isset($this->_o)) + if (! isset($this->_o)) return $this->_o = new Imagick($this->file_name(FALSE)); } diff --git a/app/Traits/FindQuicktimeAtoms.php b/app/Traits/FindQuicktimeAtoms.php new file mode 100644 index 0000000..626b725 --- /dev/null +++ b/app/Traits/FindQuicktimeAtoms.php @@ -0,0 +1,104 @@ +filename,'r'); + fseek($fh,$offset); + } + + $result = collect(); + + while ($rp < $size) { + $read = $atom ? substr($atom,$rp,8) : fread($fh,8); + $rp += strlen($read); + + $header = unpack('Nsize/a4atom',$read); + + // For mdat atoms, if size = 1, the true size is in the 64 bit extended header + if (($header['atom'] === 'mdat') && ($header['size'] === 1)) + throw new \Exception(sprintf('%s:! We havent handed large QT files yet.',self::LOGKEY)); + + // Load our class for this supplier + $class = $class_prefix.$header['atom']; + + $data = $atom + ? substr($atom,$rp,$header['size']-8) + : ($header['size']-8 && ($header['size']-8 <= self::BLOCK_SIZE) ? fread($fh,$header['size']-8) : NULL); + + if ($header['size'] >= 8) { + $o = class_exists($class) + ? new $class($offset+$rp,$header['size']-8,$this->filename,$data,$passthru) + : new $unknown($offset+$rp,$header['size']-8,$this->filename,$header['atom'],$data); + + $result->push($o); + + $rp += $header['size']-8; + + // Only need to seek if we didnt read all the data + if ((! $atom) && ($header['size'] > self::BLOCK_SIZE)) + fseek($fh,$offset+$rp); + + } else { + dd([get_class($this) => $data]); + } + + // Work out if data from the last atom next to be passed onto the next one + if ($callback) + $passthru = $callback($o); + } + + if (! $atom) { + fclose($fh); + unset($fh); + } + + return $result; + } + + /** + * Recursively look through our object hierarchy of atoms for a specific atom class + * + * @param string $subatom + * @param int|NULL $expect + * @param int $depth + * @return Collection|Atom|NULL + * @throws \Exception + */ + public function find_atoms(string $subatom,?int $expect=NULL,int $depth=100): Collection|Atom|NULL + { + if (! isset($this->atoms) || ($depth < 0)) + return NULL; + + $subatomo = $this->atoms->filter(fn($item)=>get_class($item)===$subatom); + + $subatomo = $subatomo + ->merge($this->atoms->map(fn($item)=>$item->find_atoms($subatom,NULL,$depth-1)) + ->filter(fn($item)=>$item ? $item->count() : NULL) + ->flatten()); + + if (! $subatomo->count()) + return $subatomo; + + if ($expect && ($subatomo->count() !== $expect)) + throw new \Exception(sprintf('! Expected %d subatoms of %s, but have %d',$expect,$subatom,$subatomo->count())); + + return ($expect === 1) ? $subatomo->pop() : $subatomo; + } +} \ No newline at end of file diff --git a/composer.json b/composer.json index 4d5ab10..c5b509d 100644 --- a/composer.json +++ b/composer.json @@ -6,10 +6,10 @@ "license": "MIT", "require": { "php": "^8.3", + "ext-fileinfo": "*", "ext-pdo": "*", "ext-imagick": "*", "ext-pgsql": "*", - "james-heinrich/getid3": "^1.9", "laravel/framework": "^11.0", "laravel/ui": "^4.5", "leenooks/laravel": "^11.1", diff --git a/helpers.php b/helpers.php index e394276..fbb4510 100644 --- a/helpers.php +++ b/helpers.php @@ -1,8 +1,98 @@ = 0x20 && $i <= 0x7E) ? chr($i) : $pad; + } + + $hex = str_split(bin2hex($data),$width*2); + $chars = str_split(strtr($data,$from,$to),$width); + + $offset = 0; + foreach ($hex as $i => $line) { + $result .= sprintf('%08X: %-48s [%s]%s', + $offset, + substr_replace(implode(' ',str_split($line,2)),' ',8*3,0), + $chars[$i], + $newline); + + $offset += $width; + } + + return $result; + } +} + +/** + * Determine if an int is valid for this environment + */ +if (! function_exists('is_valid_int')) { + function is_valid_int(mixed $num): bool + { + /* + // check if integers are 64-bit + static $hasINT64 = NULL; + + if ($hasINT64 === NULL) { + $hasINT64 = is_int(pow(2, 31)); // 32-bit int are limited to (2^31)-1 + + if ((! $hasINT64) && (! defined('PHP_INT_MIN'))) + define('PHP_INT_MIN', ~PHP_INT_MAX); + } + */ + + // if integers are 64-bit - no other check required + return (($num <= PHP_INT_MAX) && ($num >= PHP_INT_MIN)); + } +} + +if (! function_exists('fixed_point_int_to_float')) { + function fixed_point_int_to_float(string $rawdata): float + { + if (! strlen($rawdata)) + return 0; + + $len = strlen($rawdata); + if ($len&($len-1) === 0) + throw new \Exception('Rawdata len must be a power of 2'); + + // Take the first part + $value = 0; + for ($i=0; $i<$len/2; $i++) + $value += ord($rawdata[$len/2+$i])*pow(256,(strlen($rawdata)-1-$i)); + + // Take the second part + $dec = 0; + for ($i=0; $i<$len/2; $i++) + $dec += ord($rawdata[$i])*pow(256,(strlen($rawdata)-1-$i)); + + return $value+$dec/pow(2,8*$len/2); + } +} + +if (! function_exists('pascal_string')) { + function pascal_string(string $str): string + { + $len = ord(substr($str,0,1)); + + return substr($str,1,$len); + } } \ No newline at end of file diff --git a/resources/views/components/map.blade.php b/resources/views/components/map.blade.php new file mode 100644 index 0000000..8c43dc3 --- /dev/null +++ b/resources/views/components/map.blade.php @@ -0,0 +1,19 @@ +
+ +@section('page-scripts') + + + + +@append \ No newline at end of file diff --git a/resources/views/components/photo/thumbnail.blade.php b/resources/views/components/photo/thumbnail.blade.php new file mode 100644 index 0000000..c46ba57 --- /dev/null +++ b/resources/views/components/photo/thumbnail.blade.php @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/resources/views/components/thumbnail.blade.php b/resources/views/components/thumbnail.blade.php deleted file mode 100644 index 6f6fe0c..0000000 --- a/resources/views/components/thumbnail.blade.php +++ /dev/null @@ -1,3 +0,0 @@ - - - \ No newline at end of file diff --git a/resources/views/photo/view.blade.php b/resources/views/photo/view.blade.php index 17d73df..736f306 100644 --- a/resources/views/photo/view.blade.php +++ b/resources/views/photo/view.blade.php @@ -8,7 +8,6 @@ Photo #{{ $po->id }} @endsection @section('contentheader_description') - @endsection @section('page_title') #{{ $po->id }} @@ -17,22 +16,10 @@ @section('main-content')
- +
@@ -53,7 +40,7 @@
NEW Filename
{{ $po->file_name() }}
@endif -
Size
{{ number_format($po->file_size,0) }}
+
Size
{{ number_format($po->file_size) }}
Dimensions
{{ $po->dimensions }} @ {{ $po->orientation }}

Date Taken
{{ $po->created?->format('Y-m-d H:i:s') }}
@@ -66,7 +53,7 @@ @if(! $po->gps) UNKNOWN @else -
+ @endif
@endif @@ -112,26 +99,4 @@
-@endsection - -@section('page-scripts') - @if($po->gps) - - - - - @endif -@append \ No newline at end of file +@endsection \ No newline at end of file diff --git a/resources/views/photo/widgets/thumbnail.blade.php b/resources/views/photo/widgets/thumbnail.blade.php index 8d3bc74..3a98d35 100644 --- a/resources/views/photo/widgets/thumbnail.blade.php +++ b/resources/views/photo/widgets/thumbnail.blade.php @@ -25,7 +25,7 @@
- +