Spaces:
Running
Running
| /** | |
| * Extract the final frame from a video element as a Blob | |
| * This is used to create continuity between video segments | |
| * Similar to the approach in sora-extend, but using browser Canvas API | |
| */ | |
| export async function extractFinalFrame( | |
| videoElement: HTMLVideoElement | |
| ): Promise<Blob> { | |
| return new Promise((resolve, reject) => { | |
| try { | |
| // Create a canvas with the video's dimensions | |
| const canvas = document.createElement('canvas'); | |
| canvas.width = videoElement.videoWidth; | |
| canvas.height = videoElement.videoHeight; | |
| const ctx = canvas.getContext('2d'); | |
| if (!ctx) { | |
| reject(new Error('Could not get canvas context')); | |
| return; | |
| } | |
| // Draw the current frame onto the canvas | |
| ctx.drawImage(videoElement, 0, 0, canvas.width, canvas.height); | |
| // Convert canvas to blob | |
| canvas.toBlob( | |
| (blob) => { | |
| if (blob) { | |
| resolve(blob); | |
| } else { | |
| reject(new Error('Failed to create blob from canvas')); | |
| } | |
| }, | |
| 'image/jpeg', | |
| 0.95 // High quality for better continuity | |
| ); | |
| } catch (error) { | |
| reject(error); | |
| } | |
| }); | |
| } | |
| /** | |
| * Seek to the last frame of a video and extract it | |
| * Useful for extracting the final frame before the video ends naturally | |
| */ | |
| export async function seekAndExtractFinalFrame( | |
| videoElement: HTMLVideoElement | |
| ): Promise<Blob> { | |
| return new Promise((resolve, reject) => { | |
| const handleSeeked = async () => { | |
| try { | |
| const blob = await extractFinalFrame(videoElement); | |
| videoElement.removeEventListener('seeked', handleSeeked); | |
| resolve(blob); | |
| } catch (error) { | |
| videoElement.removeEventListener('seeked', handleSeeked); | |
| reject(error); | |
| } | |
| }; | |
| videoElement.addEventListener('seeked', handleSeeked); | |
| // Seek to 50ms before the end to ensure we get a valid frame | |
| const targetTime = Math.max(0, videoElement.duration - 0.05); | |
| videoElement.currentTime = targetTime; | |
| }); | |
| } | |
| /** | |
| * Create a preview URL for a blob (useful for debugging) | |
| */ | |
| export function createBlobPreviewUrl(blob: Blob): string { | |
| return URL.createObjectURL(blob); | |
| } | |
| /** | |
| * Revoke a blob URL to free memory | |
| */ | |
| export function revokeBlobUrl(url: string): void { | |
| URL.revokeObjectURL(url); | |
| } | |
| /** | |
| * Download a blob as a file (useful for debugging/testing) | |
| */ | |
| export function downloadBlob(blob: Blob, filename: string): void { | |
| const url = createBlobPreviewUrl(blob); | |
| const a = document.createElement('a'); | |
| a.href = url; | |
| a.download = filename; | |
| document.body.appendChild(a); | |
| a.click(); | |
| document.body.removeChild(a); | |
| revokeBlobUrl(url); | |
| } | |