Implement our own quicktime parser
This commit is contained in:
parent
a3013078e0
commit
f9cdc8f9d2
@ -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'));
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
@ -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();
|
||||
}
|
||||
|
42
app/Media/Base.php
Normal file
42
app/Media/Base.php
Normal file
@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace App\Media;
|
||||
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
abstract class Base
|
||||
{
|
||||
protected const BLOCK_SIZE = 4096;
|
||||
|
||||
/** Full path to the file */
|
||||
protected string $filename;
|
||||
protected int $filesize;
|
||||
protected string $type;
|
||||
|
||||
public function __construct(string $filename,string $type)
|
||||
{
|
||||
Log::info(sprintf('Create a media type [%s] for [%s]',get_class($this),$filename));
|
||||
|
||||
$this->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);
|
||||
}
|
||||
}
|
||||
}
|
30
app/Media/Factory.php
Normal file
30
app/Media/Factory.php
Normal file
@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace App\Media;
|
||||
|
||||
use Illuminate\Support\Arr;
|
||||
|
||||
class Factory {
|
||||
private const LOGKEY = 'MF-';
|
||||
|
||||
/**
|
||||
* @var array event type to event class mapping
|
||||
*/
|
||||
public const map = [
|
||||
'video/quicktime' => 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);
|
||||
}
|
||||
}
|
121
app/Media/QuickTime.php
Normal file
121
app/Media/QuickTime.php
Normal file
@ -0,0 +1,121 @@
|
||||
<?php
|
||||
|
||||
namespace App\Media;
|
||||
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
use App\Media\QuickTime\Atoms\{mdat,moov,Unknown};
|
||||
use App\Traits\FindQuicktimeAtoms;
|
||||
|
||||
// https://developer.apple.com/documentation/quicktime-file-format/quicktime_movie_files
|
||||
|
||||
class QuickTime extends Base {
|
||||
use FindQuicktimeAtoms;
|
||||
|
||||
private const LOGKEY = 'MFQ';
|
||||
|
||||
private Collection $atoms;
|
||||
|
||||
private const atom_classes = 'App\\Media\\QuickTime\\Atoms\\';
|
||||
|
||||
public function __construct(string $filename,string $type)
|
||||
{
|
||||
parent::__construct($filename,$type);
|
||||
|
||||
// Parse the atoms
|
||||
$this->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');
|
||||
}
|
||||
}
|
144
app/Media/QuickTime/Atom.php
Normal file
144
app/Media/QuickTime/Atom.php
Normal file
@ -0,0 +1,144 @@
|
||||
<?php
|
||||
|
||||
namespace App\Media\QuickTime;
|
||||
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
use App\Media\QuickTime\Atoms\moov\{mvhd,trak};
|
||||
use App\Traits\FindQuicktimeAtoms;
|
||||
|
||||
abstract class Atom
|
||||
{
|
||||
use FindQuicktimeAtoms;
|
||||
|
||||
protected const record_size = 16384;
|
||||
|
||||
protected const BLOCK_SIZE = 4096;
|
||||
|
||||
protected int $offset;
|
||||
protected int $size;
|
||||
protected string $filename;
|
||||
|
||||
private mixed $fh;
|
||||
private int $fp;
|
||||
protected Collection $cache;
|
||||
protected Collection $atoms;
|
||||
|
||||
public function __construct(int $offset,int $size,string $filename)
|
||||
{
|
||||
$this->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();
|
||||
}
|
||||
}
|
59
app/Media/QuickTime/Atoms/SubAtom.php
Normal file
59
app/Media/QuickTime/Atoms/SubAtom.php
Normal file
@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
namespace App\Media\QuickTime\Atoms;
|
||||
|
||||
use Illuminate\Support\Collection;
|
||||
use Leenooks\Traits\ObjectIssetFix;
|
||||
|
||||
use App\Media\QuickTime\Atom;
|
||||
use App\Media\QuickTime\Atoms\moov\trak\tkhd;
|
||||
|
||||
abstract class SubAtom extends Atom
|
||||
{
|
||||
use ObjectIssetFix;
|
||||
|
||||
protected ?string $unused_data;
|
||||
|
||||
protected const atom_record = [
|
||||
'version'=>['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;
|
||||
}
|
||||
}
|
23
app/Media/QuickTime/Atoms/Unknown.php
Normal file
23
app/Media/QuickTime/Atoms/Unknown.php
Normal file
@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace App\Media\QuickTime\Atoms;
|
||||
|
||||
// An atom we dont know how to handle
|
||||
|
||||
use App\Media\QuickTime\Atom;
|
||||
|
||||
class Unknown extends Atom
|
||||
{
|
||||
private string $atom;
|
||||
|
||||
public function __construct(int $offset,int $size,string $filename,string $atom,?string $data)
|
||||
{
|
||||
parent::__construct($offset,$size,$filename,$data);
|
||||
|
||||
$this->atom = $atom;
|
||||
|
||||
// For debugging
|
||||
if (FALSE)
|
||||
$this->debug = hex_dump($data ?: $this->data());
|
||||
}
|
||||
}
|
12
app/Media/QuickTime/Atoms/free.php
Normal file
12
app/Media/QuickTime/Atoms/free.php
Normal file
@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
namespace App\Media\QuickTime\Atoms;
|
||||
|
||||
// Unused space available in file.
|
||||
|
||||
use App\Media\QuickTime\Atom;
|
||||
|
||||
class free extends Atom
|
||||
{
|
||||
|
||||
}
|
34
app/Media/QuickTime/Atoms/ftyp.php
Normal file
34
app/Media/QuickTime/Atoms/ftyp.php
Normal file
@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace App\Media\QuickTime\Atoms;
|
||||
|
||||
// File type compatibility—identifies the file type and differentiates it from similar file types,
|
||||
// such as MPEG-4 files and JPEG-2000 files.
|
||||
|
||||
// The file type atom has an atom type value of 'ftyp' and contains the following fields:
|
||||
// Size, Type, Major brand, Minor version, Compatible brands.
|
||||
|
||||
use Illuminate\Support\Arr;
|
||||
|
||||
use App\Media\QuickTime\Atom;
|
||||
|
||||
class ftyp extends Atom
|
||||
{
|
||||
protected const unpack = [
|
||||
'major'=>['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');
|
||||
}
|
||||
}
|
42
app/Media/QuickTime/Atoms/mdat.php
Normal file
42
app/Media/QuickTime/Atoms/mdat.php
Normal file
@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace App\Media\QuickTime\Atoms;
|
||||
|
||||
// Movie sample data—media samples such as video frames and groups of audio samples.
|
||||
// Usually this data can be interpreted only by using the movie resource.
|
||||
|
||||
use Illuminate\Support\Arr;
|
||||
|
||||
use App\Media\QuickTime\Atom;
|
||||
|
||||
class mdat extends Atom
|
||||
{
|
||||
/**
|
||||
* Calculate the signature of the data
|
||||
*
|
||||
* @param string $alg
|
||||
* @return string
|
||||
*/
|
||||
public function signature(string $alg='sha1'): string
|
||||
{
|
||||
if (! Arr::has($this->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'];
|
||||
}
|
||||
}
|
19
app/Media/QuickTime/Atoms/moov.php
Normal file
19
app/Media/QuickTime/Atoms/moov.php
Normal file
@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Media\QuickTime\Atoms;
|
||||
|
||||
// Movie resource metadata about the movie (number and type of tracks, location of sample data, and so on).
|
||||
// Describes where the movie data can be found and how to interpret it.
|
||||
|
||||
use App\Media\QuickTime\Atom;
|
||||
|
||||
class moov extends Atom
|
||||
{
|
||||
private const subatom_classes = 'App\\Media\\QuickTime\\Atoms\\moov\\';
|
||||
|
||||
public function __construct(int $offset,int $size,string $filename,?string $data) {
|
||||
parent::__construct($offset,$size,$filename);
|
||||
|
||||
$this->atoms = $this->get_atoms(self::subatom_classes,Unknown::class,$offset,$size,$data);
|
||||
}
|
||||
}
|
12
app/Media/QuickTime/Atoms/moov/free.php
Normal file
12
app/Media/QuickTime/Atoms/moov/free.php
Normal file
@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
namespace App\Media\QuickTime\Atoms\moov;
|
||||
|
||||
// Unused space available in file.
|
||||
|
||||
use App\Media\QuickTime\Atoms\SubAtom;
|
||||
|
||||
class free extends SubAtom
|
||||
{
|
||||
|
||||
}
|
47
app/Media/QuickTime/Atoms/moov/meta.php
Normal file
47
app/Media/QuickTime/Atoms/moov/meta.php
Normal file
@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
namespace App\Media\QuickTime\Atoms\moov;
|
||||
|
||||
use Illuminate\Support\Arr;
|
||||
|
||||
use App\Media\QuickTime\Atoms\moov\meta\{ilst,keys};
|
||||
use App\Media\QuickTime\Atoms\{SubAtom,Unknown};
|
||||
|
||||
class meta extends SubAtom
|
||||
{
|
||||
private const subatom_classes = 'App\\Media\\QuickTime\\Atoms\\moov\\meta\\';
|
||||
|
||||
public function __construct(int $offset,int $size,string $filename,?string $data)
|
||||
{
|
||||
parent::__construct($offset,$size,$filename);
|
||||
|
||||
$this->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);
|
||||
}
|
||||
}
|
||||
}
|
12
app/Media/QuickTime/Atoms/moov/meta/free.php
Normal file
12
app/Media/QuickTime/Atoms/moov/meta/free.php
Normal file
@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
namespace App\Media\QuickTime\Atoms\moov\meta;
|
||||
|
||||
// Unused space available in file.
|
||||
|
||||
use App\Media\QuickTime\Atoms\SubAtom;
|
||||
|
||||
class free extends SubAtom
|
||||
{
|
||||
|
||||
}
|
41
app/Media/QuickTime/Atoms/moov/meta/ilst.php
Normal file
41
app/Media/QuickTime/Atoms/moov/meta/ilst.php
Normal file
@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace App\Media\QuickTime\Atoms\moov\meta;
|
||||
|
||||
// Item LiST container atom
|
||||
|
||||
use App\Media\QuickTime\Atoms\SubAtom;
|
||||
|
||||
class ilst extends SubAtom
|
||||
{
|
||||
public function __construct(int $offset,int $size,string $filename,?string $data)
|
||||
{
|
||||
parent::__construct($offset,$size,$filename);
|
||||
|
||||
$ptr = 0;
|
||||
while ($ptr < strlen($data)) {
|
||||
$key_size = unpack('Nsize',substr($data,$ptr,4));
|
||||
// Sometimes atoms are terminated with a 0
|
||||
if ($key_size['size'] === 0) {
|
||||
$ptr += 4;
|
||||
continue;
|
||||
}
|
||||
|
||||
$a = unpack(sprintf('a4name/a%ddata',$key_size['size']-8),substr($data,$ptr+4,4+$key_size['size']-8));
|
||||
$ptr += $key_size['size'];
|
||||
|
||||
// If we didnt get the right amount of data, something is wrong.
|
||||
if (strlen($a['data']) < $key_size['size']-8)
|
||||
break;
|
||||
|
||||
$b = unpack(sprintf('Nsize/a4name/a%ddata',strlen($a['data'])-8),$a['data']);
|
||||
|
||||
if ($b['name'] !== 'data')
|
||||
throw new \Exception('Parsing of ILST got data that wasnt expected');
|
||||
$c = unpack(sprintf('a4language/a4unknown/a%ddata',strlen($b['data'])-8),$b['data']);
|
||||
|
||||
// heirachy, name, size, offset
|
||||
$this->cache = $this->cache->push($c['data']);
|
||||
}
|
||||
}
|
||||
}
|
26
app/Media/QuickTime/Atoms/moov/meta/keys.php
Normal file
26
app/Media/QuickTime/Atoms/moov/meta/keys.php
Normal file
@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace App\Media\QuickTime\Atoms\moov\meta;
|
||||
|
||||
// The metadata item keys atom holds a list of the metadata keys that may be present in the metadata atom.
|
||||
// This list is indexed starting with 1; 0 is a reserved index value. The metadata item keys atom is a full atom with an atom type of "keys".
|
||||
|
||||
use App\Media\QuickTime\Atoms\SubAtom;
|
||||
|
||||
class keys extends SubAtom
|
||||
{
|
||||
public function __construct(int $offset,int $size,string $filename,?string $data)
|
||||
{
|
||||
parent::__construct($offset,$size,$filename);
|
||||
|
||||
$this->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']));
|
||||
}
|
||||
}
|
||||
}
|
56
app/Media/QuickTime/Atoms/moov/mvhd.php
Normal file
56
app/Media/QuickTime/Atoms/moov/mvhd.php
Normal file
@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
namespace App\Media\QuickTime\Atoms\moov;
|
||||
|
||||
// Specifies the characteristics of an entire QuickTime movie.
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Support\Arr;
|
||||
|
||||
use App\Media\QuickTime\Atoms\SubAtom;
|
||||
|
||||
class mvhd extends SubAtom
|
||||
{
|
||||
protected const unpack = [
|
||||
'version'=>['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);
|
||||
}
|
||||
}
|
||||
}
|
19
app/Media/QuickTime/Atoms/moov/trak.php
Normal file
19
app/Media/QuickTime/Atoms/moov/trak.php
Normal file
@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Media\QuickTime\Atoms\moov;
|
||||
|
||||
// An atom that defines a single track of a movie
|
||||
|
||||
use App\Media\QuickTime\Atoms\SubAtom;
|
||||
use App\Media\QuickTime\Atoms\Unknown;
|
||||
|
||||
class trak extends SubAtom
|
||||
{
|
||||
private const subatom_classes = 'App\\Media\\QuickTime\\Atoms\\moov\\trak\\';
|
||||
|
||||
public function __construct(int $offset,int $size,string $filename,?string $data) {
|
||||
parent::__construct($offset,$size,$filename);
|
||||
|
||||
$this->atoms = $this->get_atoms(self::subatom_classes,Unknown::class,$offset,$size,$data);
|
||||
}
|
||||
}
|
33
app/Media/QuickTime/Atoms/moov/trak/mdia.php
Normal file
33
app/Media/QuickTime/Atoms/moov/trak/mdia.php
Normal file
@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace App\Media\QuickTime\Atoms\moov\trak;
|
||||
|
||||
use App\Media\QuickTime\Atoms\moov\trak\mdia\hdlr;
|
||||
use App\Media\QuickTime\Atoms\SubAtom;
|
||||
use App\Media\QuickTime\Atoms\Unknown;
|
||||
|
||||
class mdia extends SubAtom
|
||||
{
|
||||
private const subatom_classes = 'App\\Media\\QuickTime\\Atoms\\moov\\trak\\mdia\\';
|
||||
|
||||
|
||||
public function __construct(int $offset,int $size,string $filename,?string $data)
|
||||
{
|
||||
parent::__construct($offset,$size,$filename);
|
||||
|
||||
// We'll find atoms
|
||||
$this->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());
|
||||
}
|
||||
}
|
32
app/Media/QuickTime/Atoms/moov/trak/mdia/hdlr.php
Normal file
32
app/Media/QuickTime/Atoms/moov/trak/mdia/hdlr.php
Normal file
@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace App\Media\QuickTime\Atoms\moov\trak\mdia;
|
||||
|
||||
use App\Media\QuickTime\Atoms\SubAtom;
|
||||
|
||||
class hdlr extends SubAtom
|
||||
{
|
||||
protected const unpack = [
|
||||
'version'=>['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());
|
||||
}
|
||||
}
|
39
app/Media/QuickTime/Atoms/moov/trak/mdia/minf.php
Normal file
39
app/Media/QuickTime/Atoms/moov/trak/mdia/minf.php
Normal file
@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
namespace App\Media\QuickTime\Atoms\moov\trak\mdia;
|
||||
|
||||
use Leenooks\Traits\ObjectIssetFix;
|
||||
|
||||
use App\Media\QuickTime\Atoms\SubAtom;
|
||||
use App\Media\QuickTime\Atoms\Unknown;
|
||||
|
||||
class minf extends SubAtom
|
||||
{
|
||||
use ObjectIssetFix;
|
||||
|
||||
private const subatom_classes = 'App\\Media\\QuickTime\\Atoms\\moov\\trak\\mdia\\minf\\';
|
||||
|
||||
protected ?string $type;
|
||||
public function __construct(int $offset,int $size,string $filename,?string $data,string $arg=NULL)
|
||||
{
|
||||
parent::__construct($offset,$size,$filename);
|
||||
$this->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);
|
||||
}
|
||||
}
|
||||
}
|
10
app/Media/QuickTime/Atoms/moov/trak/mdia/minf/hdlr.php
Normal file
10
app/Media/QuickTime/Atoms/moov/trak/mdia/minf/hdlr.php
Normal file
@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace App\Media\QuickTime\Atoms\moov\trak\mdia\minf;
|
||||
|
||||
use App\Media\QuickTime\Atoms\moov\trak\mdia\hdlr as MdiaHdlr;
|
||||
|
||||
class hdlr extends MdiaHdlr
|
||||
{
|
||||
|
||||
}
|
22
app/Media/QuickTime/Atoms/moov/trak/mdia/minf/stbl.php
Normal file
22
app/Media/QuickTime/Atoms/moov/trak/mdia/minf/stbl.php
Normal file
@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace App\Media\QuickTime\Atoms\moov\trak\mdia\minf;
|
||||
|
||||
use App\Media\QuickTime\Atoms\SubAtom;
|
||||
use App\Media\QuickTime\Atoms\Unknown;
|
||||
|
||||
class stbl extends SubAtom
|
||||
{
|
||||
private const subatom_classes = 'App\\Media\\QuickTime\\Atoms\\moov\\trak\\mdia\\minf\\stbl\\';
|
||||
|
||||
public function __construct(int $offset,int $size,string $filename,?string $data,?string $arg=NULL)
|
||||
{
|
||||
parent::__construct($offset,$size,$filename);
|
||||
|
||||
$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());
|
||||
}
|
||||
}
|
104
app/Media/QuickTime/Atoms/moov/trak/mdia/minf/stbl/stsd.php
Normal file
104
app/Media/QuickTime/Atoms/moov/trak/mdia/minf/stbl/stsd.php
Normal file
@ -0,0 +1,104 @@
|
||||
<?php
|
||||
|
||||
namespace App\Media\QuickTime\Atoms\moov\trak\mdia\minf\stbl;
|
||||
|
||||
use Illuminate\Support\Arr;
|
||||
|
||||
use App\Media\QuickTime\Atoms\SubAtom;
|
||||
|
||||
class stsd extends SubAtom
|
||||
{
|
||||
protected const unpack = [
|
||||
'size'=>['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);
|
||||
}
|
||||
}
|
||||
}
|
32
app/Media/QuickTime/Atoms/moov/trak/mdia/minf/stbl/stts.php
Normal file
32
app/Media/QuickTime/Atoms/moov/trak/mdia/minf/stbl/stts.php
Normal file
@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace App\Media\QuickTime\Atoms\moov\trak\mdia\minf\stbl;
|
||||
|
||||
use App\Media\QuickTime\Atoms\SubAtom;
|
||||
|
||||
class stts extends SubAtom
|
||||
{
|
||||
public function __construct(int $offset,int $size,string $filename,?string $data)
|
||||
{
|
||||
parent::__construct($offset,$size,$filename);
|
||||
|
||||
$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->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();
|
||||
}
|
||||
}
|
10
app/Media/QuickTime/Atoms/moov/trak/meta.php
Normal file
10
app/Media/QuickTime/Atoms/moov/trak/meta.php
Normal file
@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace App\Media\QuickTime\Atoms\moov\trak;
|
||||
|
||||
use App\Media\QuickTime\Atoms\moov\meta as MoovMeta;
|
||||
|
||||
class meta extends MoovMeta
|
||||
{
|
||||
|
||||
}
|
55
app/Media/QuickTime/Atoms/moov/trak/tkhd.php
Normal file
55
app/Media/QuickTime/Atoms/moov/trak/tkhd.php
Normal file
@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
namespace App\Media\QuickTime\Atoms\moov\trak;
|
||||
|
||||
// An atom that specifies the characteristics of a single track within a movie
|
||||
|
||||
use Illuminate\Support\Arr;
|
||||
|
||||
use App\Media\QuickTime\Atoms\SubAtom;
|
||||
|
||||
class tkhd extends SubAtom
|
||||
{
|
||||
protected const unpack = [
|
||||
'version'=>['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);
|
||||
}
|
||||
}
|
||||
}
|
12
app/Media/QuickTime/Atoms/skip.php
Normal file
12
app/Media/QuickTime/Atoms/skip.php
Normal file
@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
namespace App\Media\QuickTime\Atoms;
|
||||
|
||||
// Unused space available in file.
|
||||
|
||||
use App\Media\QuickTime\Atom;
|
||||
|
||||
class skip extends Atom
|
||||
{
|
||||
|
||||
}
|
13
app/Media/QuickTime/Atoms/wide.php
Normal file
13
app/Media/QuickTime/Atoms/wide.php
Normal file
@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
namespace App\Media\QuickTime\Atoms;
|
||||
|
||||
// Reserved space—can be overwritten by an extended size field if the following atom exceeds 2^32 bytes,
|
||||
// without displacing the contents of the following atom.
|
||||
|
||||
use App\Media\QuickTime\Atom;
|
||||
|
||||
class wide extends Atom
|
||||
{
|
||||
|
||||
}
|
24
app/Media/Unknown.php
Normal file
24
app/Media/Unknown.php
Normal file
@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace App\Media;
|
||||
|
||||
/**
|
||||
* This represents a media type that we dont know how to parse
|
||||
*/
|
||||
class Unknown extends Base {
|
||||
private const LOGKEY = 'MF?';
|
||||
|
||||
private string $type;
|
||||
|
||||
public function __construct(string $filename,string $type)
|
||||
{
|
||||
parent::__construct($filename);
|
||||
|
||||
$this->type = $type;
|
||||
}
|
||||
|
||||
public function __get(string $key): mixed
|
||||
{
|
||||
return NULL;
|
||||
}
|
||||
}
|
@ -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));
|
||||
}
|
||||
|
||||
|
104
app/Traits/FindQuicktimeAtoms.php
Normal file
104
app/Traits/FindQuicktimeAtoms.php
Normal file
@ -0,0 +1,104 @@
|
||||
<?php
|
||||
|
||||
namespace App\Traits;
|
||||
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
use App\Media\QuickTime\Atom;
|
||||
|
||||
trait FindQuicktimeAtoms
|
||||
{
|
||||
protected function get_atoms(string $class_prefix,string $unknown,int $offset,int $size,string $atom=NULL,string $passthru=NULL,\Closure $callback=NULL): Collection
|
||||
{
|
||||
// List of atoms
|
||||
// File Type atom should proceed move, movie data, preview. free space atoms, if it exists.
|
||||
// Can assume it doesnt exist if those other atoms are discovered first.
|
||||
// Atoms can be present in any order
|
||||
// Must contain a movie atom.
|
||||
// Movie data atoms can exceed 2^32.
|
||||
|
||||
$rp = 0;
|
||||
if (! $atom) {
|
||||
$fh = fopen($this->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;
|
||||
}
|
||||
}
|
@ -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",
|
||||
|
92
helpers.php
92
helpers.php
@ -1,8 +1,98 @@
|
||||
<?php
|
||||
|
||||
if (! function_exists('stringtrim')) {
|
||||
function stringtrim(string $string,int $chrs=6)
|
||||
function stringtrim(string $string,int $chrs=6): string
|
||||
{
|
||||
return sprintf('%s...%s',substr($string,0,$chrs),substr($string,-1*$chrs));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Dump out data into a hex dump
|
||||
*/
|
||||
if (! function_exists('hex_dump')) {
|
||||
function hex_dump($data,$newline="\n",$width=16): string
|
||||
{
|
||||
$result = '';
|
||||
|
||||
$pad = '.'; # padding for non-visible characters
|
||||
$to = $from = '';
|
||||
|
||||
for ($i=0; $i<=0xFF; $i++) {
|
||||
$from .= chr($i);
|
||||
$to .= ($i >= 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);
|
||||
}
|
||||
}
|
19
resources/views/components/map.blade.php
Normal file
19
resources/views/components/map.blade.php
Normal file
@ -0,0 +1,19 @@
|
||||
<div id="map" class="w-100" style="height: 30em;"></div>
|
||||
|
||||
@section('page-scripts')
|
||||
<script type="text/javascript" src="//unpkg.com/maplibre-gl/dist/maplibre-gl.js"></script>
|
||||
<link type='text/css' href="//unpkg.com/maplibre-gl/dist/maplibre-gl.css" rel='stylesheet'>
|
||||
|
||||
<script type="text/javascript">
|
||||
var map = new maplibregl.Map({
|
||||
container: 'map', // container id
|
||||
style: '/js/maplibre-style.json', // style URL
|
||||
center: [{{ $o->gps_lon }}, {{ $o->gps_lat }}], // starting position [lng, lat]
|
||||
zoom: 12 // starting zoom
|
||||
});
|
||||
|
||||
let marker = new maplibregl.Marker()
|
||||
.setLngLat([{{ $o->gps_lon }}, {{ $o->gps_lat }}])
|
||||
.addTo(map);
|
||||
</script>
|
||||
@append
|
3
resources/views/components/photo/thumbnail.blade.php
Normal file
3
resources/views/components/photo/thumbnail.blade.php
Normal file
@ -0,0 +1,3 @@
|
||||
<a href="{{ url('p/view',$id) }}" target="{{ $id }}">
|
||||
<img class="pb-3 w-100" src="{{ url('p/thumbnail',$id) }}">
|
||||
</a>
|
@ -1,3 +0,0 @@
|
||||
<a href="{{ url('p/view',$id) }}" target="{{ $id }}">
|
||||
<img class="p-3 w-100" src="{{ url('p/thumbnail',$id) }}">
|
||||
</a>
|
@ -8,7 +8,6 @@
|
||||
Photo #{{ $po->id }}
|
||||
@endsection
|
||||
@section('contentheader_description')
|
||||
|
||||
@endsection
|
||||
@section('page_title')
|
||||
#{{ $po->id }}
|
||||
@ -17,22 +16,10 @@
|
||||
@section('main-content')
|
||||
<div class="row">
|
||||
<div class="col-3">
|
||||
<x-thumbnail :id="$po->id"/>
|
||||
<x-photo.thumbnail :id="$po->id"/>
|
||||
|
||||
<div class="pagination justify-content-center">
|
||||
<ul class="pagination">
|
||||
<li class="page-item @disabled(! $x=$po->previous())">
|
||||
<a class="page-link" href="{{ $x ? url('p/info',$x->id) : '#' }}"><<</a>
|
||||
</li>
|
||||
|
||||
<li class="page-item active">
|
||||
<span class="page-link">{{ $po->id }}</span>
|
||||
</li>
|
||||
|
||||
<li class="page-item @disabled(! $x=$po->next())">
|
||||
<a class="page-link" href="{{ $x ? url('p/info',$x->id) : '#' }}">>></a>
|
||||
</li>
|
||||
</ul>
|
||||
<x-pagination.nav :o="$po" pre="p"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -53,7 +40,7 @@
|
||||
<dt>NEW Filename</dt><dd>{{ $po->file_name() }}<dd>
|
||||
@endif
|
||||
|
||||
<dt>Size</dt><dd>{{ number_format($po->file_size,0) }}<dd>
|
||||
<dt>Size</dt><dd>{{ number_format($po->file_size) }}<dd>
|
||||
<dt>Dimensions</dt><dd>{{ $po->dimensions }} @ {{ $po->orientation }}<dd>
|
||||
<hr>
|
||||
<dt>Date Taken</dt><dd>{{ $po->created?->format('Y-m-d H:i:s') }}<dd>
|
||||
@ -66,7 +53,7 @@
|
||||
@if(! $po->gps)
|
||||
UNKNOWN
|
||||
@else
|
||||
<div id="map" class="w-100" style="height: 30em;"></div>
|
||||
<x-map :o="$po"/>
|
||||
@endif
|
||||
</dd>
|
||||
@endif
|
||||
@ -113,25 +100,3 @@
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
@section('page-scripts')
|
||||
@if($po->gps)
|
||||
<script type="text/javascript" src="//unpkg.com/maplibre-gl/dist/maplibre-gl.js"></script>
|
||||
<link type='text/css' href="//unpkg.com/maplibre-gl/dist/maplibre-gl.css" rel='stylesheet'>
|
||||
|
||||
<script type="text/javascript">
|
||||
const key = 'hspQjLANaPwdHUrvUcsf';
|
||||
|
||||
var map = new maplibregl.Map({
|
||||
container: 'map', // container id
|
||||
style: '/js/maplibre-style.json', // style URL
|
||||
center: [{{ $po->gps_lon }}, {{ $po->gps_lat }}], // starting position [lng, lat]
|
||||
zoom: 12 // starting zoom
|
||||
});
|
||||
|
||||
let marker = new maplibregl.Marker()
|
||||
.setLngLat([{{ $po->gps_lon }}, {{ $po->gps_lat }}])
|
||||
.addTo(map);
|
||||
</script>
|
||||
@endif
|
||||
@append
|
@ -25,7 +25,7 @@
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<x-thumbnail :id="$o->id"/>
|
||||
<x-photo.thumbnail :id="$o->id"/>
|
||||
</div>
|
||||
|
||||
<div class="card-footer card-comments">
|
||||
|
@ -17,8 +17,8 @@ Route::get('/v/duplicates/{id?}',[VideoController::class,'duplicates'])
|
||||
|
||||
Route::view('/p/info/{po}','photo.view')
|
||||
->where('po','[0-9]+');
|
||||
Route::get('/v/info/{o}',[VideoController::class,'info'])
|
||||
->where('o','[0-9]+');
|
||||
Route::view('/v/info/{vo}','video.view')
|
||||
->where('vo','[0-9]+');
|
||||
Route::get('/p/thumbnail/{o}',[PhotoController::class,'thumbnail'])
|
||||
->where('o','[0-9]+');
|
||||
Route::get('/p/view/{o}',[PhotoController::class,'view'])
|
||||
|
Loading…
Reference in New Issue
Block a user