1.8.0: Videos aus der Baustellen-App annehmen und abspielbar ausliefern [deploy]
All checks were successful
Deploy bericht / deploy (push) Successful in 13s
All checks were successful
Deploy bericht / deploy (push) Successful in 13s
Eddy: „Bei Fotos aufnehmen auch Videos unterstützen bitte".
- orders.php upload_photo nimmt mp4/m4v/mov/webm, am Inhalt geprüft
(ftyp ohne Bildmarke bzw. EBML), Name video_…; taken_at wird bei
Videos zum Dateidatum. Zu groß für PHP → 413 mit Meldung.
- Aufnahmezeit eines Videos aus dem MP4-Kopf (moov → mvhd), auch wenn
moov hinter den Videodaten steht.
- photo.php liefert Teilbereiche (Range → 206): Videos spielen sofort
und lassen sich spulen.
- config.php meldet upload_max für die Höchstlänge in der App.
- Bericht-Editor: 🎬-Kachel für Videos; in Berichte übernehmen geht
weiterhin nur mit Bild/PDF.
Im Android-Emulator über die App geprüft (Aufnahme, Funkloch, Galerie,
Abspielen, Löschen).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
1d3070e6e2
commit
c4c4aba891
8 changed files with 253 additions and 16 deletions
|
|
@ -165,9 +165,12 @@ API-Endpoints (neue):
|
|||
- GET /api/orders.php — Aufträge des Users (filter: q, open)
|
||||
- GET /api/orders.php?id=<id> — Order-Detail
|
||||
- GET /api/orders.php?id=<id>&action=photos — Order-Dateien (Bilder/Audio/PDFs/Dokumente)
|
||||
- POST /api/orders.php?id=<id>&action=upload_photo — Foto ODER Video (1.8.0: mp4/m4v/mov/webm, Inhalt geprüft, `video_…`,
|
||||
optional taken_at; zu groß → 413). Videos: Aufnahmezeit aus mvhd (`bericht_video_taken_at`), sonst Dateidatum
|
||||
- GET /api/config.php — photo_maxside, photo_quality, upload_max (Bytes, 1.8.0)
|
||||
- POST /api/orders.php?action=create — Neuer Order (socid, title, ref_client, date); haengt seit 1.7.3 KEINE
|
||||
Kontakte selbst an (Standards kommen vom Kern-Trigger ContactRoles, DeliveryInvoiceAddress 1.8.0 ordnet sie)
|
||||
- GET /api/photo.php?relpath=<path> — Datei-Serving (Whitelist: facture|commande|propal|bericht)
|
||||
- GET /api/photo.php?relpath=<path> — Datei-Serving (Whitelist: facture|commande|propal|bericht), seit 1.8.0 mit Range (206) für Videos
|
||||
- GET /api/photo.php?relpath=<path>&size=thumb&w=<px> — Thumbnail (1.5.1, GD + Cache + ETag)
|
||||
- POST /api/pages.php?action=signature&bericht_id=<id> — Touch-Unterschrift
|
||||
- DELETE /api/pages.php?id=<id> — Seite löschen
|
||||
|
|
|
|||
26
ChangeLog.md
26
ChangeLog.md
|
|
@ -1,5 +1,31 @@
|
|||
# Changelog
|
||||
|
||||
## 1.8.0 — 2026-10-19
|
||||
|
||||
### Videos aus der Baustellen-App
|
||||
„Bei Fotos aufnehmen auch Videos unterstützen bitte" (Eddy, 19.10.2026). Die Baustellen-App filmt jetzt
|
||||
(Kamera mit Umschalter Foto | Video, Galerie, Teilen); das Modul nimmt die Videos an und liefert sie aus.
|
||||
|
||||
- **`api/orders.php?action=upload_photo`** nimmt neben jpg/png auch **mp4, m4v, mov, webm**. Geprüft wird am
|
||||
Inhalt (`bericht_ist_video()`): MP4/MOV brauchen den Kasten `ftyp` ohne Bildmarke (AVIF/HEIC als „.mp4"
|
||||
werden abgelehnt), WebM den EBML-Kopf. Name `video_<Datum>_<uniqid>.<ext>`, Ablage und ECM-Index wie Fotos
|
||||
(Beilage → Reiter). Die AVIF/WebP-Umwandlung läuft für Videos nicht.
|
||||
- **Aufnahmezeit**: optional `taken_at` (Unix-Zeit) wird bei Videos zum Dateidatum — ein im Funkloch gefilmtes
|
||||
Video sortiert sich sonst unter der späteren Sendezeit ein. Gelesen wird die Zeit beim Auflisten zuerst aus
|
||||
dem MP4 selbst (`bericht_video_taken_at()`: moov → mvhd → creation_time, `moov` auch hinter den Videodaten);
|
||||
die Android-Fotoauswahl liefert als Dateidatum nur den Moment der Auswahl.
|
||||
- **Zu groß für PHP** (upload_max_filesize/post_max_size) → **HTTP 413** mit „Datei zu groß — höchstens N MB"
|
||||
statt „file fehlt". Die App wertet 413 als endgültig und schickt das Video nicht immer wieder.
|
||||
- **`api/config.php`** meldet `upload_max` (Bytes, `bericht_upload_max_bytes()`); die App richtet danach die
|
||||
Höchstlänge eines Videos aus und lehnt zu große Galerie-Videos gleich ab.
|
||||
- **`api/photo.php`** liefert Teilbereiche (**Range → 206**, `Accept-Ranges: bytes`): Videos spielen sofort und
|
||||
lassen sich spulen, ohne vorher komplett geladen zu werden. Mehrfach-Bereiche → ganze Datei (zulässig).
|
||||
- **Bericht-Editor**: Video-Anhänge zeigen 🎬 statt 📎. In einen Bericht übernehmen lässt sich ein Video nicht —
|
||||
`ajax/add_attachment.php` nimmt weiterhin nur Bild/PDF und meldet das; das PDF kann an Videos nicht scheitern.
|
||||
- Getestet im Android-Emulator (Chrome 153) über die App: MP4 H.264/AAC kommt an, Range-Anfragen 206,
|
||||
im Funkloch gefilmtes Video nach Netz-Rückkehr hochgeladen und nach Aufnahmezeit sortiert, 124-MB-Video
|
||||
bei 100-MB-Grenze abgelehnt. Inhaltsprüfung per CLI: MP4 ✓, AVIF als .mp4 ✗, WebM ✓, GIF als .mp4 ✗, altes MOV ✓.
|
||||
|
||||
## 1.7.5 — 2026-10-19
|
||||
|
||||
### WebP-Bilder werden beim Hochladen umgewandelt statt abgelehnt
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ Bilder und PDFs lassen sich im Browser annotieren (Pfeile, Kreise, Rechtecke, Te
|
|||
- **Bild einer Seite austauschen** ohne sie zu löschen — Reihenfolge, Notiz und Anmerkungen bleiben erhalten. Bei Raster-Layouts lässt sich jeder Platz einzeln tauschen, direkt auf der Seite anklickbar.
|
||||
- **Ziehen und Ablegen**: Kachel auf die Seitenliste legt neue Seiten an, Kachel auf die Arbeitsfläche ersetzt das Bild der offenen Seite, Kachel auf einen Raster-Platz setzt genau diesen. Dateien vom Rechner auf die Anhänge-Spalte werden hochgeladen.
|
||||
- **Mobiler Upload per QR-Code** — Handy scannt, lädt Fotos direkt in den offenen Bericht (zeitlich begrenzte Token)
|
||||
- **Videos aus der Baustellen-App (1.8.0)** — `orders.php?action=upload_photo` nimmt mp4/m4v/mov/webm (Inhalt geprüft, Name `video_…`, Aufnahmezeit aus `taken_at` bzw. dem MP4-Kopf), `photo.php` liefert Teilbereiche (Range) zum Abspielen und Spulen, `config.php` meldet `upload_max`, zu groß → 413. Im Editor 🎬-Kachel, nicht in Berichte übernehmbar.
|
||||
- **AVIF- und WebP-Bilder (1.7.4/1.7.5)** — beim Hochladen (Editor, PWA, Handy-Upload) am Inhalt erkannt und in JPG bzw. PNG (mit Transparenz) umgewandelt (`lib/bericht_avif.lib.php`, `bericht_upload_bild_umwandeln()`, KB #1698, #447). Bewegte WebP werden mit Meldung abgelehnt.
|
||||
- **Ablage am Beleg als Beilage (1.7.2)** — Fotos, Notizen, Sprachnotizen, Scans und Bericht-PDFs, die im Belegordner landen, stehen im ECM-Index als `uploaded` (`lib/bericht_index.lib.php`): nur im Reiter „Verknüpfte Dokumente", nie vorne im Kasten der Belegkarte (KB #1641)
|
||||
- **Notizen pro Seite** — werden im finalen PDF unten auf der Seite gedruckt
|
||||
|
|
|
|||
|
|
@ -8,6 +8,9 @@
|
|||
* anpasst. 0 = Originalgroesse (1:1), das ist die Vorgabe.
|
||||
* photo_quality — JPEG-Qualitaet in Prozent; greift nur, wenn tatsaechlich
|
||||
* umgerechnet wird (also bei photo_maxside > 0).
|
||||
* upload_max — groesste Datei in Bytes, die der Server annimmt (PHP-Grenzen,
|
||||
* seit 1.8.0). Die PWA richtet die Hoechstlaenge eines Videos danach
|
||||
* aus und lehnt zu grosse Videos aus der Galerie gleich ab.
|
||||
*/
|
||||
require_once __DIR__.'/_inc.php';
|
||||
|
||||
|
|
@ -16,4 +19,5 @@ api_authenticate();
|
|||
api_ok(array(
|
||||
'photo_maxside' => getDolGlobalInt('BERICHT_PHOTO_MAXSIDE', 0),
|
||||
'photo_quality' => getDolGlobalInt('BERICHT_PHOTO_QUALITY', 100),
|
||||
'upload_max' => bericht_upload_max_bytes(),
|
||||
));
|
||||
|
|
|
|||
|
|
@ -6,8 +6,10 @@
|
|||
* GET /api/orders.php?id=<id>&action=photos
|
||||
* Liefert die Anhang-Bilder/PDFs eines Auftrags.
|
||||
* POST /api/orders.php?id=<id>&action=upload_photo
|
||||
* multipart: file=<binary> — fügt ein Foto zum Bericht des Auftrags hinzu
|
||||
* (legt automatisch einen Bericht an wenn keiner existiert)
|
||||
* multipart: file=<binary> — legt ein Foto oder Video im Auftragsordner ab
|
||||
* (Fotos jpg/png, AVIF/WebP werden umgewandelt; Videos mp4/m4v/mov/webm seit 1.8.0,
|
||||
* Inhalt geprüft, Name video_…). Optional taken_at=<Unix-Zeit> = Aufnahmezeit eines
|
||||
* Videos, wird zum Dateidatum. Zu groß für PHP → 413.
|
||||
* POST /api/orders.php?action=create
|
||||
* JSON-Body: { socid, ref_client, title?, note_private?, date?, date_livraison?, validate? }
|
||||
* - ref_client ("Ihr Zeichen") ist PFLICHT (Geschäftsregel; Dolibarr selbst verlangt es nicht).
|
||||
|
|
@ -225,8 +227,27 @@ if ($action === 'photos') {
|
|||
|
||||
if ($action === 'upload_photo' && $_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
if (!$user->hasRight('bericht', 'write')) api_fail('Schreibrechte fehlen', 403);
|
||||
if (empty($_FILES['file']['tmp_name'])) api_fail('file fehlt');
|
||||
if (empty($_FILES['file']['tmp_name'])) {
|
||||
// Zu groß für PHP: bei upload_max_filesize meldet $_FILES den Fehler, bei post_max_size
|
||||
// kommt gar nichts an. 413 ist für die App ein dauerhafter Fehler — sonst schickte sie
|
||||
// ein zu großes Video immer wieder über den Mobilfunk.
|
||||
$max = bericht_upload_max_bytes();
|
||||
$php_fehler = (int) ($_FILES['file']['error'] ?? 0);
|
||||
if (in_array($php_fehler, array(UPLOAD_ERR_INI_SIZE, UPLOAD_ERR_FORM_SIZE), true)
|
||||
|| (empty($_FILES) && $max > 0 && (int) ($_SERVER['CONTENT_LENGTH'] ?? 0) > $max)) {
|
||||
api_fail('Datei zu groß — höchstens '.round($max / 1048576).' MB', 413);
|
||||
}
|
||||
api_fail('file fehlt');
|
||||
}
|
||||
|
||||
$orig = dol_sanitizeFileName($_FILES['file']['name']);
|
||||
$ext = strtolower(pathinfo($orig, PATHINFO_EXTENSION));
|
||||
$ist_video = in_array($ext, bericht_video_endungen(), true);
|
||||
|
||||
if ($ist_video) {
|
||||
// Videos aus der Kamera der App, der Galerie oder per Teilen (seit 1.8.0)
|
||||
if (!bericht_ist_video($_FILES['file']['tmp_name'], $ext)) api_fail('Kein gültiges Video');
|
||||
} else {
|
||||
// AVIF/WebP (Galerie/Teilen aus dem Netz, auch unter „.jpg") vor der Prüfung in JPG/PNG umwandeln
|
||||
require_once __DIR__.'/../lib/bericht_avif.lib.php';
|
||||
$bild_fehler = bericht_upload_bild_umwandeln($_FILES['file']);
|
||||
|
|
@ -236,6 +257,7 @@ if ($action === 'upload_photo' && $_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||
$orig = dol_sanitizeFileName($_FILES['file']['name']);
|
||||
$ext = strtolower(pathinfo($orig, PATHINFO_EXTENSION));
|
||||
if (!in_array($ext, array('jpg', 'jpeg', 'png'))) api_fail('Dateityp nicht unterstützt');
|
||||
}
|
||||
|
||||
// Foto direkt in den Dolibarr-Standard-Auftragsordner speichern (kein Bericht nötig)
|
||||
$upload_dir = $conf->commande->multidir_output[$cmd->entity].'/'.dol_sanitizeFileName($cmd->ref);
|
||||
|
|
@ -255,9 +277,20 @@ if ($action === 'upload_photo' && $_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||
));
|
||||
}
|
||||
|
||||
$filename = 'foto_'.dol_print_date(dol_now(), '%Y%m%d_%H%M%S').'_'.uniqid().'.'.$ext;
|
||||
$filename = ($ist_video ? 'video_' : 'foto_').dol_print_date(dol_now(), '%Y%m%d_%H%M%S').'_'.uniqid().'.'.$ext;
|
||||
$target = $upload_dir.'/'.$filename;
|
||||
if (!move_uploaded_file($_FILES['file']['tmp_name'], $target)) api_fail('Upload fehlgeschlagen', 500);
|
||||
|
||||
// Videos tragen keine EXIF-Zeit; die Liste sortiert sie nach dem Dateidatum. Im Funkloch
|
||||
// gefilmt und Stunden später gesendet, stünde dort die Sendezeit — die App schickt die
|
||||
// Aufnahmezeit mit (Galerie: Dateidatum des Videos), sie wird zum Dateidatum. Nur plausible Werte.
|
||||
if ($ist_video) {
|
||||
$aufgenommen = (int) GETPOST('taken_at', 'int');
|
||||
if ($aufgenommen > dol_now() - 400 * 86400 && $aufgenommen < dol_now() + 300) {
|
||||
@touch($target, $aufgenommen);
|
||||
clearstatcache(true, $target);
|
||||
}
|
||||
}
|
||||
bericht_als_beilage_indexieren($target, $cmd); // Beilage → nur im Reiter (KB #1641)
|
||||
|
||||
$relpath = str_replace(DOL_DATA_ROOT.'/', '', $target);
|
||||
|
|
|
|||
|
|
@ -123,10 +123,50 @@ $download = !empty($_GET['download']);
|
|||
$filename = $download ? basename($full) : '';
|
||||
|
||||
header('Content-Type: '.$mime);
|
||||
header('Content-Length: '.filesize($full));
|
||||
header('Accept-Ranges: bytes');
|
||||
header('Cache-Control: private, max-age=3600');
|
||||
if ($download) {
|
||||
header('Content-Disposition: attachment; filename="'.addslashes($filename).'"');
|
||||
}
|
||||
|
||||
// Teilbereiche (Range, seit 1.8.0): Ein Video aus der Baustellen-App spielt sonst erst, wenn
|
||||
// es komplett geladen ist, und lässt sich nicht spulen. Nur ein einzelner Bereich — bei
|
||||
// mehreren (selten) geht wie bisher die ganze Datei raus, das ist erlaubt.
|
||||
$groesse = (int) filesize($full);
|
||||
$von = 0;
|
||||
$bis = $groesse - 1;
|
||||
if (!empty($_SERVER['HTTP_RANGE']) && preg_match('/^bytes=(\d*)-(\d*)$/', trim($_SERVER['HTTP_RANGE']), $m)
|
||||
&& ($m[1] !== '' || $m[2] !== '')) {
|
||||
if ($m[1] === '') {
|
||||
$von = max(0, $groesse - (int) $m[2]); // „bytes=-500": die letzten 500 Bytes
|
||||
} else {
|
||||
$von = (int) $m[1];
|
||||
if ($m[2] !== '') $bis = min((int) $m[2], $groesse - 1);
|
||||
}
|
||||
if ($von > $bis || $von >= $groesse) {
|
||||
http_response_code(416);
|
||||
header('Content-Range: bytes */'.$groesse);
|
||||
exit;
|
||||
}
|
||||
http_response_code(206);
|
||||
header('Content-Range: bytes '.$von.'-'.$bis.'/'.$groesse);
|
||||
}
|
||||
$laenge = $bis - $von + 1;
|
||||
header('Content-Length: '.$laenge);
|
||||
|
||||
if ($von === 0 && $laenge === $groesse) {
|
||||
readfile($full);
|
||||
exit;
|
||||
}
|
||||
$fh = fopen($full, 'rb');
|
||||
if ($fh === false) exit;
|
||||
fseek($fh, $von);
|
||||
$rest = $laenge;
|
||||
while ($rest > 0 && !feof($fh)) {
|
||||
$stueck = fread($fh, min(1048576, $rest));
|
||||
if ($stueck === false || $stueck === '') break;
|
||||
echo $stueck;
|
||||
$rest -= strlen($stueck);
|
||||
}
|
||||
fclose($fh);
|
||||
exit;
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ class modBericht extends DolibarrModules
|
|||
|
||||
$this->editor_name = 'Alles Watt läuft';
|
||||
$this->editor_url = '';
|
||||
$this->version = '1.7.5';
|
||||
$this->version = '1.8.0';
|
||||
$this->const_name = 'MAIN_MODULE_'.strtoupper($this->name);
|
||||
$this->picto = 'fa-file-pdf';
|
||||
|
||||
|
|
|
|||
|
|
@ -698,7 +698,9 @@ function bericht_print_attachment_list(array $by_source, array $used_paths)
|
|||
print '<canvas class="att-thumb-canvas" data-pdf-url="'.dol_escape_htmltag($urls['view']).'"></canvas>';
|
||||
print '<span class="att-type-badge">PDF</span>';
|
||||
} else {
|
||||
print '<span class="att-thumb-fallback">📎</span>';
|
||||
// Videos aus der Baustellen-App: kommen nicht in den Bericht (add_attachment nimmt nur Bild/PDF)
|
||||
$ist_video = strpos($f['mime'], 'video') === 0 || preg_match('/\.(mov|m4v)$/i', $f['filename']); // .mov kennt dol_mimetype nicht
|
||||
print '<span class="att-thumb-fallback">'.($ist_video ? '🎬' : '📎').'</span>';
|
||||
}
|
||||
if ($is_used) print '<span class="att-used-badge" title="Diese Datei ist bereits im Bericht">✓</span>';
|
||||
print '</div>';
|
||||
|
|
@ -964,6 +966,8 @@ function bericht_file_taken_at($fullpath, $fallback_date = null)
|
|||
$ts = null;
|
||||
if (preg_match('/\.jpe?g$/i', $fullpath)) {
|
||||
$ts = bericht_jpeg_taken_at($fullpath);
|
||||
} elseif (preg_match('/\.(mp4|m4v|mov)$/i', $fullpath)) {
|
||||
$ts = bericht_video_taken_at($fullpath);
|
||||
}
|
||||
if (!$ts) $ts = $fallback_date ? (int) $fallback_date : @filemtime($fullpath);
|
||||
|
||||
|
|
@ -1644,3 +1648,129 @@ function bericht_find_identical_file($dir, $tmpfile)
|
|||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Videos, die die Baustellen-App annimmt (Kamera der App, Galerie, Teilen).
|
||||
* MP4 kommt aus der App-Kamera (Chrome/Android), MOV vom iPhone, WebM als Rückfall älterer Browser.
|
||||
*/
|
||||
function bericht_video_endungen()
|
||||
{
|
||||
return array('mp4', 'm4v', 'mov', 'webm');
|
||||
}
|
||||
|
||||
/**
|
||||
* Prüft am Inhalt, ob eine hochgeladene Datei wirklich ein Video der genannten Art ist —
|
||||
* die Endung allein sagt nichts.
|
||||
*
|
||||
* MP4/M4V/MOV (ISO-Basisformat): ab Byte 4 steht der Kasten „ftyp"; ältere QuickTime-Dateien
|
||||
* beginnen auch mit „moov", „mdat", „wide", „free" oder „skip". Bild-Marken im ftyp (AVIF, HEIC)
|
||||
* sind keine Videos. WebM (Matroska): EBML-Kopf 1A 45 DF A3.
|
||||
*
|
||||
* @param string $datei Pfad der hochgeladenen Datei
|
||||
* @param string $ext Endung in Kleinbuchstaben
|
||||
* @return bool
|
||||
*/
|
||||
function bericht_ist_video($datei, $ext)
|
||||
{
|
||||
$kopf = @file_get_contents($datei, false, null, 0, 12);
|
||||
if ($kopf === false || strlen($kopf) < 12) return false;
|
||||
if ($ext === 'webm') return substr($kopf, 0, 4) === "\x1A\x45\xDF\xA3";
|
||||
|
||||
$kasten = substr($kopf, 4, 4);
|
||||
if ($kasten === 'ftyp') {
|
||||
$marke = strtolower(substr($kopf, 8, 4));
|
||||
return !in_array($marke, array('avif', 'avis', 'heic', 'heix', 'mif1', 'msf1'), true);
|
||||
}
|
||||
return $ext === 'mov' && in_array($kasten, array('moov', 'mdat', 'wide', 'free', 'skip'), true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Größte Datei, die PHP über einen Upload annimmt (in Bytes): das Kleinere aus
|
||||
* upload_max_filesize und post_max_size. Die Baustellen-App richtet danach die
|
||||
* Höchstlänge ihrer Videos aus (api/config.php).
|
||||
*
|
||||
* @return int 0 = unbekannt
|
||||
*/
|
||||
function bericht_upload_max_bytes()
|
||||
{
|
||||
$werte = array();
|
||||
foreach (array('upload_max_filesize', 'post_max_size') as $schluessel) {
|
||||
$wert = trim((string) ini_get($schluessel));
|
||||
if ($wert === '') continue;
|
||||
$zahl = (float) $wert;
|
||||
switch (strtolower(substr($wert, -1))) {
|
||||
case 'g': $zahl *= 1024;
|
||||
// no break
|
||||
case 'm': $zahl *= 1024;
|
||||
// no break
|
||||
case 'k': $zahl *= 1024;
|
||||
}
|
||||
if ($zahl > 0) $werte[] = (int) $zahl;
|
||||
}
|
||||
return $werte ? min($werte) : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Aufnahmezeit eines MP4/MOV-Videos aus dem Film-Kopf (moov → mvhd, creation_time).
|
||||
*
|
||||
* Gegenstück zu bericht_jpeg_taken_at(): Kamera-Apps und Chrome (App-Kamera) schreiben die
|
||||
* Zeit dorthin. Ein Video aus der Galerie trägt sonst als Datum nur den Moment der Auswahl —
|
||||
* die Android-Fotoauswahl liefert nicht das Aufnahmedatum (Emulator, 19.10.2026).
|
||||
* moov steht bei Kamera-Apps oft HINTER den Videodaten; die obersten Kästen werden deshalb
|
||||
* per Längenangabe übersprungen, nicht gelesen.
|
||||
*
|
||||
* @param string $file
|
||||
* @return int|null Unix-Timestamp oder null (kein mvhd, Zeit 0 oder unplausibel)
|
||||
*/
|
||||
function bericht_video_taken_at($file)
|
||||
{
|
||||
$fh = @fopen($file, 'rb');
|
||||
if (!$fh) return null;
|
||||
$groesse = (int) @filesize($file);
|
||||
|
||||
// Kasten an $pos lesen: [Typ, Gesamtlänge, Kopflänge] oder null
|
||||
$kasten = function ($pos, $ende) use ($fh) {
|
||||
if ($pos + 8 > $ende) return null;
|
||||
fseek($fh, $pos);
|
||||
$kopf = fread($fh, 16);
|
||||
if ($kopf === false || strlen($kopf) < 8) return null;
|
||||
$laenge = unpack('N', substr($kopf, 0, 4))[1];
|
||||
$kopflaenge = 8;
|
||||
if ($laenge === 1 && strlen($kopf) >= 16) { // 64-Bit-Länge
|
||||
$laenge = unpack('N', substr($kopf, 8, 4))[1] * 4294967296 + unpack('N', substr($kopf, 12, 4))[1];
|
||||
$kopflaenge = 16;
|
||||
} elseif ($laenge === 0) { // bis zum Ende
|
||||
$laenge = $ende - $pos;
|
||||
}
|
||||
if ($laenge < $kopflaenge) return null;
|
||||
return array(substr($kopf, 4, 4), $laenge, $kopflaenge);
|
||||
};
|
||||
|
||||
$ts = null;
|
||||
$pos = 0;
|
||||
while (($k = $kasten($pos, $groesse)) !== null) {
|
||||
if ($k[0] === 'moov') {
|
||||
$ende = $pos + $k[1];
|
||||
$kind = $pos + $k[2];
|
||||
while (($m = $kasten($kind, $ende)) !== null) {
|
||||
if ($m[0] === 'mvhd') {
|
||||
fseek($fh, $kind + $m[2]);
|
||||
$daten = fread($fh, 12);
|
||||
if ($daten !== false && strlen($daten) >= 8) {
|
||||
$sek = (ord($daten[0]) === 1 && strlen($daten) >= 12)
|
||||
? unpack('N', substr($daten, 4, 4))[1] * 4294967296 + unpack('N', substr($daten, 8, 4))[1]
|
||||
: unpack('N', substr($daten, 4, 4))[1];
|
||||
$unix = $sek - 2082844800; // Sekunden seit 1.1.1904 → Unix-Zeit
|
||||
if ($unix > 946684800 && $unix < time() + 86400) $ts = (int) $unix; // nach 2000, nicht in der Zukunft
|
||||
}
|
||||
break;
|
||||
}
|
||||
$kind += $m[1];
|
||||
}
|
||||
break;
|
||||
}
|
||||
$pos += $k[1];
|
||||
}
|
||||
fclose($fh);
|
||||
return $ts;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue