Spaces:
Running
Running
| <script lang="ts"> | |
| import { onMount, onDestroy } from 'svelte'; | |
| import { extractFinalFrame } from '$lib/utils/video'; | |
| export let videoUrl: string | undefined; | |
| export let onVideoEnd: ((finalFrame: Blob) => void) | undefined = undefined; | |
| export let onError: ((error: string) => void) | undefined = undefined; | |
| let videoElement: HTMLVideoElement; | |
| let isPlaying = false; | |
| let hasError = false; | |
| let errorMessage = ''; | |
| onMount(() => { | |
| if (videoElement) { | |
| videoElement.addEventListener('ended', handleVideoEnd); | |
| videoElement.addEventListener('error', handleError); | |
| } | |
| }); | |
| onDestroy(() => { | |
| if (videoElement) { | |
| videoElement.removeEventListener('ended', handleVideoEnd); | |
| videoElement.removeEventListener('error', handleError); | |
| } | |
| }); | |
| async function handleVideoEnd() { | |
| if (!videoElement) return; | |
| try { | |
| // Extract the final frame for continuity | |
| const finalFrame = await extractFinalFrame(videoElement); | |
| onVideoEnd?.(finalFrame); | |
| } catch (error) { | |
| console.error('Failed to extract final frame:', error); | |
| const errorMsg = error instanceof Error ? error.message : 'Unknown error'; | |
| handleError(new Error(`Failed to extract final frame: ${errorMsg}`)); | |
| } | |
| } | |
| function handleError(error: Event | Error) { | |
| hasError = true; | |
| if (error instanceof Error) { | |
| errorMessage = error.message; | |
| } else { | |
| const videoError = videoElement?.error; | |
| errorMessage = videoError | |
| ? `Video error (code ${videoError.code}): ${videoError.message}` | |
| : 'Failed to load video'; | |
| } | |
| onError?.(errorMessage); | |
| } | |
| function handlePlay() { | |
| isPlaying = true; | |
| } | |
| function handlePause() { | |
| isPlaying = false; | |
| } | |
| // Reset error when video URL changes | |
| $: if (videoUrl) { | |
| hasError = false; | |
| errorMessage = ''; | |
| } | |
| </script> | |
| <div class="video-player-container"> | |
| {#if videoUrl} | |
| {#if !hasError} | |
| <video | |
| bind:this={videoElement} | |
| src={videoUrl} | |
| controls | |
| autoplay | |
| class="video-player" | |
| on:play={handlePlay} | |
| on:pause={handlePause} | |
| > | |
| <track kind="captions" /> | |
| </video> | |
| {:else} | |
| <div class="error-container"> | |
| <p class="error-message">❌ {errorMessage}</p> | |
| </div> | |
| {/if} | |
| {:else} | |
| <div class="placeholder"> | |
| <p>No video loaded</p> | |
| </div> | |
| {/if} | |
| </div> | |
| <style> | |
| .video-player-container { | |
| width: 100%; | |
| max-width: 1280px; | |
| margin: 0 auto; | |
| background: #000; | |
| border-radius: 0.5rem; | |
| overflow: hidden; | |
| box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); | |
| } | |
| .video-player { | |
| width: 100%; | |
| height: auto; | |
| display: block; | |
| } | |
| .placeholder, | |
| .error-container { | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| min-height: 400px; | |
| background: #1a1a1a; | |
| color: #999; | |
| } | |
| .placeholder p { | |
| font-size: 1.1rem; | |
| margin: 0; | |
| } | |
| .error-message { | |
| color: #ff6b6b; | |
| font-size: 1rem; | |
| margin: 0; | |
| padding: 2rem; | |
| text-align: center; | |
| } | |
| @media (max-width: 768px) { | |
| .placeholder, | |
| .error-container { | |
| min-height: 250px; | |
| } | |
| .placeholder p, | |
| .error-message { | |
| font-size: 0.9rem; | |
| } | |
| } | |
| </style> | |