exists($uploadDir)) { Storage::disk('public')->makeDirectory($uploadDir); } // Buat nama file unik dengan timestamp $filename = 'esp32cam_' . Carbon::now()->format('Ymd_His') . '.jpg'; $fullPath = $uploadDir . $filename; // Ambil konten gambar dari request $imageContent = $request->getContent(); if (!empty($imageContent)) { // Simpan gambar menggunakan Laravel Storage if (Storage::disk('public')->put($fullPath, $imageContent)) { // Mengatur waktu file agar sesuai dengan waktu pengambilan saat ini $now = time(); $filePath = storage_path('app/public/' . $fullPath); @touch($filePath, $now); // Log informasi upload menggunakan Laravel Logger Log::info("ESP32-CAM image uploaded successfully", [ 'filename' => $filename, 'path' => $fullPath, 'size' => strlen($imageContent) . ' bytes', 'time' => Carbon::now()->format('Y-m-d H:i:s') ]); return response()->json([ 'status' => 'success', 'message' => 'Gambar tersimpan sebagai ' . $filename, 'path' => $fullPath, 'time' => Carbon::now()->format('Y-m-d H:i:s') ]); } throw new \Exception('Gagal menyimpan gambar'); } throw new \Exception('Tidak ada data yang diterima'); } catch (\Exception $e) { Log::error("ESP32-CAM upload error: " . $e->getMessage()); return response()->json([ 'status' => 'error', 'message' => $e->getMessage() ], 500); } } /** * Menampilkan semua foto */ public function all(Request $request) { $uploadDir = 'esp32cam/'; $files = Storage::disk('public')->files($uploadDir); // Urutkan file berdasarkan waktu modifikasi (terbaru lebih dulu) usort($files, function($a, $b) { return Storage::disk('public')->lastModified($b) - Storage::disk('public')->lastModified($a); }); $photos = []; foreach ($files as $file) { $filePath = storage_path('app/public/' . $file); $fileModTime = Storage::disk('public')->lastModified($file); $fileName = basename($file); $fileSize = round(Storage::disk('public')->size($file) / 1024, 2) . ' KB'; // Mendapatkan timestamp dari nama file (format nama file: esp32cam_YmdHis) $timestampFromFilename = null; if (preg_match('/esp32cam_(\d{8})_(\d{6})/', $fileName, $matches)) { $dateStr = $matches[1]; $timeStr = $matches[2]; $timestampFromFilename = strtotime( substr($dateStr, 0, 4) . '-' . substr($dateStr, 4, 2) . '-' . substr($dateStr, 6, 2) . ' ' . substr($timeStr, 0, 2) . ':' . substr($timeStr, 2, 2) . ':' . substr($timeStr, 4, 2) ); } // Menggunakan timestamp dari nama file jika tersedia, jika tidak gunakan waktu modifikasi $captureTimestamp = $timestampFromFilename ?: $fileModTime; $captureTime = date('Y-m-d H:i:s', $captureTimestamp); // Mendapatkan timestamp dari EXIF data jika tersedia if (function_exists('exif_read_data')) { try { $exifData = @exif_read_data($filePath); if ($exifData && isset($exifData['DateTimeOriginal'])) { // Format EXIF datetime: YYYY:MM:DD HH:MM:SS $exifTime = strtotime($exifData['DateTimeOriginal']); if ($exifTime) { $captureTimestamp = $exifTime; $captureTime = date('Y-m-d H:i:s', $exifTime); } } } catch (\Exception $e) { Log::warning("Error reading EXIF data: " . $e->getMessage()); } } // Format untuk tampilan $displayTime = date('d-m-Y H:i:s', $captureTimestamp); $photoItem = [ 'name' => $fileName, 'path' => 'storage/' . $file, 'time' => $displayTime, 'size' => $fileSize ]; $photos[] = $photoItem; } return view('photos', ['photos' => $photos]); } /** * Mengambil foto terbaru */ public function latest() { $uploadDir = 'esp32cam/'; $files = Storage::disk('public')->files($uploadDir); if (count($files) > 0) { // Urutkan file berdasarkan waktu modifikasi (terbaru lebih dulu) usort($files, function($a, $b) { return Storage::disk('public')->lastModified($b) - Storage::disk('public')->lastModified($a); }); $latestPhoto = $files[0]; // Get file path for EXIF reading $filePath = storage_path('app/public/' . $latestPhoto); $fileModTime = Storage::disk('public')->lastModified($latestPhoto); $fileSize = round(Storage::disk('public')->size($latestPhoto) / 1024, 2) . ' KB'; $fileName = basename($latestPhoto); // Mendapatkan timestamp dari nama file (format nama file: esp32cam_YmdHis) $timestampFromFilename = null; if (preg_match('/esp32cam_(\d{8})_(\d{6})/', $fileName, $matches)) { $dateStr = $matches[1]; $timeStr = $matches[2]; $timestampFromFilename = strtotime( substr($dateStr, 0, 4) . '-' . substr($dateStr, 4, 2) . '-' . substr($dateStr, 6, 2) . ' ' . substr($timeStr, 0, 2) . ':' . substr($timeStr, 2, 2) . ':' . substr($timeStr, 4, 2) ); } // Menggunakan timestamp dari nama file jika tersedia, jika tidak gunakan waktu modifikasi $captureTimestamp = $timestampFromFilename ?: $fileModTime; $captureTime = date('Y-m-d H:i:s', $captureTimestamp); // Get timestamp from EXIF data if available if (function_exists('exif_read_data')) { try { $exifData = @exif_read_data($filePath); if ($exifData && isset($exifData['DateTimeOriginal'])) { // Format EXIF datetime: YYYY:MM:DD HH:MM:SS $exifTime = strtotime($exifData['DateTimeOriginal']); if ($exifTime) { $captureTimestamp = $exifTime; $captureTime = date('Y-m-d H:i:s', $exifTime); } } } catch (\Exception $e) { Log::warning("Error reading EXIF data: " . $e->getMessage()); } } // Format untuk tampilan $displayTime = date('d-m-Y H:i:s', $captureTimestamp); return response()->json([ 'status' => 'success', 'photo' => [ 'name' => $fileName, 'path' => 'storage/' . $latestPhoto, 'time' => $displayTime, 'timestamp' => $captureTimestamp, 'size' => $fileSize ] ]); } return response()->json([ 'status' => 'error', 'message' => 'Tidak ada foto yang tersedia' ], 404); } /** * Hapus foto tertentu */ public function delete(Request $request) { $filename = $request->input('filename'); $uploadDir = 'esp32cam/'; if (Storage::disk('public')->exists($uploadDir . $filename)) { Storage::disk('public')->delete($uploadDir . $filename); return response()->json([ 'status' => 'success', 'message' => 'Foto berhasil dihapus' ]); } return response()->json([ 'status' => 'error', 'message' => 'Foto tidak ditemukan' ], 404); } /** * Membersihkan foto lama */ public function cleanup() { try { $uploadDir = 'esp32cam/'; $files = Storage::disk('public')->files($uploadDir); $deletedCount = 0; // Hapus file yang lebih lama dari 7 hari, tapi simpan minimal 10 file terakhir if (count($files) > 10) { // Urutkan file berdasarkan waktu modifikasi (terlama lebih dulu) usort($files, function($a, $b) { return Storage::disk('public')->lastModified($a) - Storage::disk('public')->lastModified($b); }); // Ambil file lama untuk dihapus (tapi sisakan 10 file terakhir) $filesToDelete = array_slice($files, 0, count($files) - 10); foreach ($filesToDelete as $file) { $fileTime = Storage::disk('public')->lastModified($file); if (time() - $fileTime > 7 * 24 * 60 * 60) { // 7 hari dalam detik Storage::disk('public')->delete($file); Log::info("Deleted old ESP32-CAM image: " . $file); $deletedCount++; } } } return response()->json([ 'status' => 'success', 'message' => 'Cleanup completed. Deleted ' . $deletedCount . ' old files.', 'remaining_files' => count($files) - $deletedCount ]); } catch (\Exception $e) { Log::error("ESP32-CAM cleanup error: " . $e->getMessage()); return response()->json([ 'status' => 'error', 'message' => $e->getMessage() ], 500); } } /** * Fetch image from ESP32-CAM web server and save it * * @param Request $request * @return \Illuminate\Http\JsonResponse */ public function fetchFromCamera(Request $request) { try { // Debug untuk melihat isi request Log::info('ESP32CAM Debug - Request Content:', [ 'all' => $request->all(), 'content' => $request->getContent(), 'content_decoded' => json_decode($request->getContent(), true), 'headers' => $request->header() ]); // Gunakan IP kamera dari request atau default $cameraIp = '192.168.1.19'; // Default IP // Coba ambil dari request JSON $requestData = json_decode($request->getContent(), true); if (isset($requestData['camera_ip']) && !empty($requestData['camera_ip'])) { $cameraIp = $requestData['camera_ip']; } Log::info('ESP32CAM Debug - Camera IP:', [ 'camera_ip' => $cameraIp, 'is_empty' => empty($cameraIp) ]); if (empty($cameraIp)) { return response()->json([ 'status' => 'error', 'message' => 'IP address ESP32-CAM diperlukan' ], 400); } // URL untuk mengambil gambar dari ESP32-CAM $captureUrl = "http://{$cameraIp}/capture"; Log::info('ESP32CAM Capture URL: ' . $captureUrl); // Ambil gambar dari ESP32-CAM dengan timeout yang lebih lama $response = Http::timeout(30)->get($captureUrl); if ($response->successful()) { // Direktori untuk menyimpan gambar $uploadDir = 'esp32cam/'; // Pastikan direktori ada di storage/app/public if (!Storage::disk('public')->exists($uploadDir)) { Storage::disk('public')->makeDirectory($uploadDir); } // Buat nama file unik dengan timestamp $filename = 'esp32cam_' . Carbon::now()->format('Ymd_His') . '.jpg'; $fullPath = $uploadDir . $filename; // Simpan gambar menggunakan Laravel Storage Storage::disk('public')->put($fullPath, $response->body()); // Mengatur waktu file agar sesuai dengan waktu pengambilan saat ini $now = time(); $filePath = storage_path('app/public/' . $fullPath); @touch($filePath, $now); // Log informasi Log::info("ESP32-CAM image fetched successfully", [ 'camera_ip' => $cameraIp, 'filename' => $filename, 'path' => $fullPath, 'size' => strlen($response->body()) . ' bytes', 'time' => Carbon::now()->format('Y-m-d H:i:s') ]); return response()->json([ 'status' => 'success', 'message' => 'Gambar berhasil diambil dari ESP32-CAM', 'filename' => $filename, 'path' => $fullPath, 'time' => Carbon::now()->format('Y-m-d H:i:s') ]); } Log::error('ESP32-CAM fetch error: Failed with status ' . $response->status()); return response()->json([ 'status' => 'error', 'message' => 'Gagal mengambil gambar dari ESP32-CAM. Status: ' . $response->status(), 'camera_ip' => $cameraIp ], 500); } catch (\Exception $e) { Log::error("ESP32-CAM fetch error: " . $e->getMessage(), [ 'camera_ip' => $cameraIp ?? '192.168.1.19', 'exception' => $e ]); return response()->json([ 'status' => 'error', 'message' => 'Gagal mengambil gambar: ' . $e->getMessage(), 'camera_ip' => $cameraIp ?? '192.168.1.19' ], 500); } } /** * Proxy untuk endpoint status dari ESP32-CAM * * @param Request $request * @return \Illuminate\Http\Response */ public function proxyStatus(Request $request) { try { $cameraIp = $request->query('ip'); if (empty($cameraIp)) { return response()->json([ 'status' => 'error', 'message' => 'IP address ESP32-CAM diperlukan' ], 400); } // Periksa apakah URL sudah mengandung http:// atau https:// if (!preg_match('/^https?:\/\//', $cameraIp)) { $statusUrl = "http://{$cameraIp}/status"; } else { // Gunakan URL langsung jika sudah termasuk protokol $statusUrl = "{$cameraIp}/status"; } Log::info('ESP32CAM Proxy Status URL: ' . $statusUrl); $response = Http::timeout(10)->get($statusUrl); if ($response->successful()) { return response($response->body(), 200) ->header('Content-Type', 'application/json'); } return response()->json([ 'status' => 'error', 'message' => 'Gagal mengakses status ESP32-CAM. Status: ' . $response->status(), 'camera_ip' => $cameraIp ], 500); } catch (\Exception $e) { Log::error("ESP32-CAM proxy status error: " . $e->getMessage()); return response()->json([ 'status' => 'error', 'message' => 'Gagal mengakses status: ' . $e->getMessage() ], 500); } } /** * Proxy untuk stream video dari ESP32-CAM * * @param Request $request * @return \Symfony\Component\HttpFoundation\StreamedResponse */ public function proxyStream(Request $request) { $cameraIp = $request->query('ip'); if (empty($cameraIp)) { return response()->json([ 'status' => 'error', 'message' => 'IP address ESP32-CAM diperlukan' ], 400); } // Periksa apakah URL sudah mengandung http:// atau https:// if (!preg_match('/^https?:\/\//', $cameraIp)) { $streamUrl = "http://{$cameraIp}/stream"; } else { // Gunakan URL langsung jika sudah termasuk protokol $streamUrl = "{$cameraIp}/stream"; } Log::info('ESP32CAM Proxy Stream URL: ' . $streamUrl); // Menggunakan streaming response untuk meneruskan MJPEG stream return response()->stream(function() use ($streamUrl) { $ch = curl_init($streamUrl); curl_setopt($ch, CURLOPT_RETURNTRANSFER, false); // Langsung kirim output ke browser curl_exec($ch); curl_close($ch); }, 200, [ 'Content-Type' => 'multipart/x-mixed-replace; boundary=frame', 'Cache-Control' => 'no-cache, private', 'Connection' => 'close', 'Pragma' => 'no-cache' ]); } /** * Proxy untuk pengaturan kamera ESP32-CAM * * @param Request $request * @return \Illuminate\Http\Response */ public function proxySettings(Request $request) { try { $cameraIp = $request->query('ip'); $resolution = $request->query('resolution'); $quality = $request->query('quality'); if (empty($cameraIp)) { return response()->json([ 'status' => 'error', 'message' => 'IP address ESP32-CAM diperlukan' ], 400); } // Periksa apakah URL sudah mengandung http:// atau https:// if (!preg_match('/^https?:\/\//', $cameraIp)) { $baseUrl = "http://{$cameraIp}"; } else { // Gunakan URL langsung jika sudah termasuk protokol $baseUrl = $cameraIp; } // Bangun URL dengan parameter yang diperlukan $settingsUrl = "{$baseUrl}/camera-settings"; $params = []; if (!empty($resolution)) { $params['resolution'] = $resolution; } if (!empty($quality)) { $params['quality'] = $quality; } if (!empty($params)) { $settingsUrl .= '?' . http_build_query($params); } Log::info('ESP32CAM Proxy Settings URL: ' . $settingsUrl); $response = Http::timeout(10)->get($settingsUrl); if ($response->successful()) { return response($response->body(), 200) ->header('Content-Type', 'application/json'); } return response()->json([ 'status' => 'error', 'message' => 'Gagal mengubah pengaturan ESP32-CAM. Status: ' . $response->status(), 'camera_ip' => $cameraIp ], 500); } catch (\Exception $e) { Log::error("ESP32-CAM proxy settings error: " . $e->getMessage()); return response()->json([ 'status' => 'error', 'message' => 'Gagal mengubah pengaturan: ' . $e->getMessage() ], 500); } } /** * Proxy untuk endpoint capture dari ESP32-CAM * * @param Request $request * @return \Illuminate\Http\Response */ public function proxyCapture(Request $request) { try { $cameraIp = $request->query('ip'); if (empty($cameraIp)) { return response()->json([ 'status' => 'error', 'message' => 'IP address ESP32-CAM diperlukan' ], 400); } // Periksa apakah URL sudah mengandung http:// atau https:// if (!preg_match('/^https?:\/\//', $cameraIp)) { $captureUrl = "http://{$cameraIp}/capture"; } else { // Gunakan URL langsung jika sudah termasuk protokol $captureUrl = "{$cameraIp}/capture"; } Log::info('ESP32CAM Proxy Capture URL: ' . $captureUrl); $response = Http::timeout(15)->get($captureUrl); if ($response->successful()) { // Jika hanya ingin menampilkan gambar tanpa menyimpan if ($request->query('display_only') === 'true') { return response($response->body(), 200) ->header('Content-Type', 'image/jpeg'); } // Jika ingin menyimpan gambar $uploadDir = 'esp32cam/'; // Pastikan direktori ada di storage/app/public if (!Storage::disk('public')->exists($uploadDir)) { Storage::disk('public')->makeDirectory($uploadDir); } // Buat nama file unik dengan timestamp $filename = 'esp32cam_' . Carbon::now()->format('Ymd_His') . '.jpg'; $fullPath = $uploadDir . $filename; // Simpan gambar menggunakan Laravel Storage Storage::disk('public')->put($fullPath, $response->body()); // Mengatur waktu file agar sesuai dengan waktu pengambilan saat ini $now = time(); $filePath = storage_path('app/public/' . $fullPath); @touch($filePath, $now); return response()->json([ 'status' => 'success', 'message' => 'Gambar berhasil diambil dan disimpan', 'filename' => $filename, 'path' => $fullPath, 'time' => Carbon::now()->format('Y-m-d H:i:s') ]); } return response()->json([ 'status' => 'error', 'message' => 'Gagal mengambil gambar dari ESP32-CAM. Status: ' . $response->status(), 'camera_ip' => $cameraIp ], 500); } catch (\Exception $e) { Log::error("ESP32-CAM proxy capture error: " . $e->getMessage()); return response()->json([ 'status' => 'error', 'message' => 'Gagal mengambil gambar: ' . $e->getMessage() ], 500); } } /** * Proxy untuk menghentikan streaming dari ESP32-CAM * * @param Request $request * @return \Illuminate\Http\Response */ public function proxyStopStream(Request $request) { try { $cameraIp = $request->query('ip'); if (empty($cameraIp)) { return response()->json([ 'status' => 'error', 'message' => 'IP address ESP32-CAM diperlukan' ], 400); } // Periksa apakah URL sudah mengandung http:// atau https:// if (!preg_match('/^https?:\/\//', $cameraIp)) { $stopStreamUrl = "http://{$cameraIp}/stopstream"; } else { // Gunakan URL langsung jika sudah termasuk protokol $stopStreamUrl = "{$cameraIp}/stopstream"; } Log::info('ESP32CAM Proxy Stop Stream URL: ' . $stopStreamUrl); $response = Http::timeout(5)->get($stopStreamUrl); if ($response->successful()) { return response()->json([ 'status' => 'success', 'message' => 'Streaming dihentikan' ]); } return response()->json([ 'status' => 'error', 'message' => 'Gagal menghentikan streaming. Status: ' . $response->status(), 'camera_ip' => $cameraIp ], 500); } catch (\Exception $e) { Log::error("ESP32-CAM proxy stop stream error: " . $e->getMessage()); return response()->json([ 'status' => 'error', 'message' => 'Gagal menghentikan streaming: ' . $e->getMessage() ], 500); } } }