json([ 'error' => [ 'message' => 'Invalid UUID format.', ], ], 400); } $video = Video::with('video_elements')->where('uuid', $uuid)->first(); if (! $video) { return response()->json([ 'error' => [ 'message' => 'Video not found.', ], ]); } return response()->json((object) [ 'success' => [ 'data' => [ 'video_elements' => $video->video_elements, ], ], ]); if (! $video_render) { return response()->json([ 'error' => [ 'message' => 'Video render not found.', ], ]); } $video = Video::where('id', $video_render->video_id)->first(); if (! $video) { return response()->json([ 'error' => [ 'message' => 'Video not found.', ], ]); } } public function startRender(Request $request) { $video_render_request = array_to_object_2025($request->all()); $video_render_action = $this->saveUserVideoRenderRequest(Auth::user(), $video_render_request); if (! $video_render_action->success) { $error_message = $video_render_action?->message ? $video_render_action->message : 'Unable to render, possibly because the video is already being rendered. Check external ID.'; return response()->json([ 'error' => [ 'message' => $error_message, ], ]); } // Create a video return response()->json((object) [ 'success' => [ 'data' => [ 'uuid' => $video_render_action->model->uuid, 'status' => $video_render_action->model->status, ], ], ]); } public function renderStatus(Request $request, string $uuid) { if (! Str::isUuid($uuid)) { return response()->json([ 'error' => [ 'message' => 'Invalid UUID.', ], ]); } $video_render = VideoRender::where('uuid', $uuid)->first(); if (! $video_render) { return response()->json([ 'error' => [ 'message' => 'Video render not found.', ], ]); } return response()->json((object) [ 'success' => [ 'data' => [ 'uuid' => $video_render->uuid, 'status' => $video_render->status, ], ], ]); } public function allRenders(Request $request) { $user = Auth::user(); $video_renders = VideoRender::where('user_id', $user->id) ->orderBy('id', 'desc')->get(); return response()->json((object) [ 'success' => [ 'data' => [ 'video_renders' => $video_renders, ], ], ]); } private function saveUserVideoRenderRequest(User $user, stdClass $video_render_request) { // check if there is an existing video render request with the same external id $video_render_is_busy = VideoRender::where('user_id', $user->id) ->where('external_id', $video_render_request->external_id) ->whereIn('status', [ RenderConstants::STATUS_PLANNED, RenderConstants::STATUS_WAITING, RenderConstants::STATUS_TRANSCRIBING, RenderConstants::STATUS_RENDERING, ])->first(); if ($video_render_is_busy) { return (object) [ 'success' => false, 'message' => 'Video is already in queue or rendering. Status: '.$video_render_is_busy->status, ]; } // dd($video_render_request); $video = $this->getUserVideoByExternalId($user, $video_render_request->external_id); $video = $this->updateVideoWithRenderRequest($video, $video_render_request); $video = $this->saveUserVideo($video); $this->saveVideoCaptions($video); try { $this->saveVideoElements($video); } catch (\Exception $e) { return (object) [ 'success' => false, 'message' => $e->getMessage(), ]; } $new_video_render = VideoRender::create([ 'user_id' => $user->id, 'video_id' => $video->id, 'external_id' => $video_render_request->external_id, 'payload' => $video_render_request, 'status' => RenderConstants::STATUS_PLANNED, ]); return (object) [ 'success' => true, 'model' => $new_video_render, ]; } private function saveVideoCaptions(Video $video) { VideoCaption::where('video_id', $video->id)->delete(); if (isset($video->payload->captions)) { foreach ($video->payload->captions as $caption) { $video_caption = new VideoCaption; $video_caption->video_id = $video->id; $video_caption->time = $caption->time; $video_caption->duration = $caption->duration; $video_caption->text = $caption->text; if (isset($caption->parameters)) { $video_caption->parameters = $caption->parameters; } $video_caption->words = $caption->words; $video_caption->save(); } } } private function saveVideoElements(Video $video) { if (isset($video->payload->elements)) { $existing_video_elements = VideoElement::where('video_id', $video->id)->get(); // Create a lookup array of existing elements by asset hash, but keep ALL matching elements $existing_elements_by_hash = []; foreach ($existing_video_elements as $existing_element) { if (! isset($existing_elements_by_hash[$existing_element->asset_hash])) { $existing_elements_by_hash[$existing_element->asset_hash] = []; } $existing_elements_by_hash[$existing_element->asset_hash][] = $existing_element; } // Track which elements we're keeping $kept_element_ids = []; // Track which hashes we've already processed to handle duplicates $processed_hashes = []; // Validate element URL if exist foreach ($video->payload->elements as $element) { if (isset($element->url)) { if (! $this->validateElementUrl($element->url)) { throw new \Exception('Invalid URL: '.$element->url); } } } // Save foreach ($video->payload->elements as $element) { $asset_hash = $this->getAssetHash($video, $element->url); if (isset($existing_elements_by_hash[$asset_hash]) && count($existing_elements_by_hash[$asset_hash]) > 0) { // Get the next unused element with this hash $unused_elements = array_filter($existing_elements_by_hash[$asset_hash], function ($elem) use ($kept_element_ids) { return ! in_array($elem->id, $kept_element_ids); }); if (count($unused_elements) > 0) { // Use the first unused element $video_element = reset($unused_elements); $kept_element_ids[] = $video_element->id; } else { // All elements with this hash are already used, create a new one $video_element = new VideoElement; $video_element->video_id = $video->id; $video_element->asset_hash = $asset_hash; $video_element->original_asset_url = $element->url; } } else { // No elements with this hash, create a new one $video_element = new VideoElement; $video_element->video_id = $video->id; $video_element->asset_hash = $asset_hash; $video_element->original_asset_url = $element->url; } if (isset($element->external_reference) && ! is_empty($element->external_reference)) { $video_element->external_reference = $element->external_reference; } $video_element->type = $element->type; $video_element->time = $element->time; $video_element->track = $element->track; $video_element->duration = $element->duration; if (isset($element->parameters)) { $video_element->parameters = $element->parameters; } $video_element->save(); // Add newly created ID to kept list if needed if (! in_array($video_element->id, $kept_element_ids)) { $kept_element_ids[] = $video_element->id; } } // Delete elements that weren't in the payload if (count($existing_video_elements) > 0) { VideoElement::where('video_id', $video->id) ->whereNotIn('id', $kept_element_ids) ->delete(); } } } private function getAssetHash(Video $video, $url) { return hash('sha256', $video->id.'-'.$url); } private function validateElementUrl(string $url) { // First check if it's a valid URL format $validator = Validator::make(['url' => $url], [ 'url' => 'required|url', ]); if ($validator->fails()) { return false; } // validate url by making a http head request, return true | false boolean try { // Using Laravel's HTTP client to make a HEAD request $response = Http::withOptions([ 'timeout' => 10, 'connect_timeout' => 5, 'verify' => true, 'http_errors' => false, // Don't throw exceptions for 4xx/5xx responses ])->head($url); // Check if the response is successful (2xx status code) or a redirect (3xx) return $response->successful(); } catch (\Exception $e) { // Catch any exceptions (connection issues, invalid URLs, etc.) return false; } } private function saveUserVideo(Video $video) { if ($video->isDirty()) { $video->save(); } return $video; } private function updateVideoWithRenderRequest(Video $video, stdClass $video_render_request) { // dd($video_render_request); $video->content_type = $video_render_request->content_type; $video->width = $video_render_request->width; $video->height = $video_render_request->height; $video->aspect_ratio = $video_render_request->aspect_ratio; $video->payload = $video_render_request; $video->render_settings = (object) [ 'video_bitrate' => $video_render_request->video_bitrate, 'audio_bitrate' => $video_render_request->audio_bitrate, 'fps' => $video_render_request->fps, ]; return $video; } private function getUserVideoByExternalId(User $user, string $external_id) { $video = Video::where('user_id', $user->id) ->where('external_id', $external_id) ->first(); if (! $video) { $video = new Video; $video->user_id = $user->id; $video->external_id = $external_id; } return $video; } }