Spaces:
Running
Running
error processing video, shared array buffer is not defined, please find out whats wrong with the video processing and fix it please. - Initial Deployment
b2c4dd7 verified | <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Video Cutter & Trimmer | HuggingFace Space</title> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> | |
| <script src="https://cdn.jsdelivr.net/npm/@ffmpeg/ffmpeg@0.11.6/dist/ffmpeg.min.js"></script> | |
| <script> | |
| tailwind.config = { | |
| theme: { | |
| extend: { | |
| colors: { | |
| hf: { | |
| purple: '#7b3ae1', | |
| dark: '#0b0f19', | |
| light: '#f5f5f7', | |
| } | |
| } | |
| } | |
| } | |
| } | |
| </script> | |
| <style> | |
| .video-container { | |
| aspect-ratio: 16/9; | |
| } | |
| .timeline-handle { | |
| width: 12px; | |
| height: 20px; | |
| top: -8px; | |
| } | |
| .timeline-handle::after { | |
| content: ''; | |
| position: absolute; | |
| width: 2px; | |
| height: 10px; | |
| background: white; | |
| top: 5px; | |
| left: 5px; | |
| } | |
| .progress-bar { | |
| height: 4px; | |
| } | |
| @media (max-width: 768px) { | |
| .controls-grid { | |
| grid-template-columns: repeat(2, minmax(0, 1fr)); | |
| } | |
| } | |
| </style> | |
| </head> | |
| <body class="bg-hf-dark text-hf-light min-h-screen"> | |
| <div class="container mx-auto px-4 py-8 max-w-6xl"> | |
| <!-- Header --> | |
| <header class="flex flex-col md:flex-row justify-between items-center mb-8 gap-4"> | |
| <div class="flex items-center gap-3"> | |
| <div class="bg-hf-purple p-2 rounded-lg"> | |
| <i class="fas fa-video text-white text-xl"></i> | |
| </div> | |
| <h1 class="text-2xl font-bold">Video Cutter & Trimmer</h1> | |
| </div> | |
| <div class="flex gap-2"> | |
| <button class="bg-hf-purple hover:bg-purple-800 px-4 py-2 rounded-lg flex items-center gap-2 transition"> | |
| <i class="fas fa-share"></i> Share | |
| </button> | |
| <button class="bg-gray-700 hover:bg-gray-600 px-4 py-2 rounded-lg flex items-center gap-2 transition"> | |
| <i class="fas fa-star"></i> Like | |
| </button> | |
| </div> | |
| </header> | |
| <!-- Main Content --> | |
| <div class="grid grid-cols-1 lg:grid-cols-3 gap-8"> | |
| <!-- Left Panel - Upload & Settings --> | |
| <div class="lg:col-span-1 bg-gray-800 rounded-xl p-6"> | |
| <h2 class="text-xl font-semibold mb-4 flex items-center gap-2"> | |
| <i class="fas fa-upload"></i> Upload Video | |
| </h2> | |
| <!-- File Upload --> | |
| <div class="border-2 border-dashed border-gray-600 rounded-lg p-6 text-center mb-6 cursor-pointer hover:border-hf-purple transition" | |
| id="dropZone"> | |
| <input type="file" id="fileInput" class="hidden" accept=".mp4,.avi,.mkv,.mov"> | |
| <div class="flex flex-col items-center justify-center"> | |
| <i class="fas fa-cloud-upload-alt text-4xl text-hf-purple mb-3"></i> | |
| <p class="mb-2">Drag & drop your video file here</p> | |
| <p class="text-sm text-gray-400 mb-3">or</p> | |
| <button class="bg-hf-purple hover:bg-purple-800 px-4 py-2 rounded-lg transition"> | |
| Browse Files | |
| </button> | |
| </div> | |
| <p class="text-xs text-gray-400 mt-3">Supports: MP4, AVI, MKV, MOV</p> | |
| </div> | |
| <!-- Video Info --> | |
| <div id="videoInfo" class="hidden mb-6"> | |
| <div class="flex justify-between items-center mb-2"> | |
| <h3 class="font-medium">File Info</h3> | |
| <button id="clearFile" class="text-red-400 hover:text-red-300 text-sm"> | |
| <i class="fas fa-times"></i> Clear | |
| </button> | |
| </div> | |
| <div class="bg-gray-700 rounded-lg p-3"> | |
| <div class="flex justify-between mb-1"> | |
| <span class="text-gray-400">Name:</span> | |
| <span id="fileName" class="font-mono">video.mp4</span> | |
| </div> | |
| <div class="flex justify-between mb-1"> | |
| <span class="text-gray-400">Size:</span> | |
| <span id="fileSize" class="font-mono">24.5 MB</span> | |
| </div> | |
| <div class="flex justify-between mb-1"> | |
| <span class="text-gray-400">Duration:</span> | |
| <span id="fileDuration" class="font-mono">02:45</span> | |
| </div> | |
| <div class="flex justify-between"> | |
| <span class="text-gray-400">Resolution:</span> | |
| <span id="fileResolution" class="font-mono">1920x1080</span> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Output Settings --> | |
| <div class="mb-6"> | |
| <h3 class="font-medium mb-3 flex items-center gap-2"> | |
| <i class="fas fa-cog"></i> Output Settings | |
| </h3> | |
| <div class="space-y-4"> | |
| <div> | |
| <label class="block text-sm text-gray-400 mb-1">Format</label> | |
| <select class="w-full bg-gray-700 border border-gray-600 rounded-lg px-3 py-2"> | |
| <option value="mp4">MP4 (Default)</option> | |
| <option value="avi">AVI</option> | |
| <option value="mkv">MKV</option> | |
| <option value="mov">MOV</option> | |
| </select> | |
| </div> | |
| <div> | |
| <label class="block text-sm text-gray-400 mb-1">Quality</label> | |
| <select class="w-full bg-gray-700 border border-gray-600 rounded-lg px-3 py-2"> | |
| <option value="original">Original (No compression)</option> | |
| <option value="high">High (90% quality)</option> | |
| <option value="medium">Medium (75% quality)</option> | |
| <option value="low">Low (50% quality)</option> | |
| </select> | |
| </div> | |
| <div class="flex items-center gap-2"> | |
| <input type="checkbox" id="preserveAudio" checked class="rounded bg-gray-700"> | |
| <label for="preserveAudio" class="text-sm">Preserve original audio quality</label> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Action Buttons --> | |
| <div class="space-y-3"> | |
| <button id="processBtn" disabled class="w-full bg-hf-purple hover:bg-purple-800 py-3 rounded-lg font-medium flex items-center justify-center gap-2 transition opacity-50"> | |
| <i class="fas fa-cut"></i> Process Video | |
| </button> | |
| <button id="downloadBtn" disabled class="w-full bg-gray-700 hover:bg-gray-600 py-3 rounded-lg font-medium flex items-center justify-center gap-2 transition opacity-50"> | |
| <i class="fas fa-download"></i> Download | |
| </button> | |
| </div> | |
| </div> | |
| <!-- Right Panel - Video Preview & Editor --> | |
| <div class="lg:col-span-2 space-y-6"> | |
| <!-- Video Preview --> | |
| <div class="bg-gray-800 rounded-xl p-6"> | |
| <h2 class="text-xl font-semibold mb-4 flex items-center gap-2"> | |
| <i class="fas fa-play"></i> Preview | |
| </h2> | |
| <div class="video-container bg-black rounded-lg overflow-hidden relative" id="videoContainer"> | |
| <video id="videoPlayer" class="w-full h-full object-contain" controls></video> | |
| <div id="noVideo" class="absolute inset-0 flex items-center justify-center"> | |
| <div class="text-center p-6"> | |
| <i class="fas fa-video-slash text-4xl text-gray-600 mb-3"></i> | |
| <p class="text-gray-400">No video loaded</p> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Timeline Editor --> | |
| <div class="bg-gray-800 rounded-xl p-6"> | |
| <h2 class="text-xl font-semibold mb-4 flex items-center gap-2"> | |
| <i class="fas fa-sliders-h"></i> Editor | |
| </h2> | |
| <!-- Trim Controls --> | |
| <div class="mb-6"> | |
| <h3 class="font-medium mb-3">Trim Video</h3> | |
| <div class="grid grid-cols-2 gap-4 mb-4"> | |
| <div> | |
| <label class="block text-sm text-gray-400 mb-1">Start Time</label> | |
| <div class="flex gap-2"> | |
| <input type="number" min="0" value="0" class="w-full bg-gray-700 border border-gray-600 rounded-lg px-3 py-2" id="startMin"> | |
| <span class="flex items-center">:</span> | |
| <input type="number" min="0" max="59" value="0" class="w-full bg-gray-700 border border-gray-600 rounded-lg px-3 py-2" id="startSec"> | |
| </div> | |
| </div> | |
| <div> | |
| <label class="block text-sm text-gray-400 mb-1">End Time</label> | |
| <div class="flex gap-2"> | |
| <input type="number" min="0" value="0" class="w-full bg-gray-700 border border-gray-600 rounded-lg px-3 py-2" id="endMin"> | |
| <span class="flex items-center">:</span> | |
| <input type="number" min="0" max="59" value="0" class="w-full bg-gray-700 border border-gray-600 rounded-lg px-3 py-2" id="endSec"> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Timeline Visualization --> | |
| <div class="mb-6"> | |
| <div class="flex justify-between mb-2"> | |
| <span id="currentTime">00:00</span> | |
| <span id="totalDuration">00:00</span> | |
| </div> | |
| <div class="relative h-2 bg-gray-700 rounded-full overflow-hidden mb-2"> | |
| <div class="absolute top-0 left-0 h-full bg-hf-purple" id="progressBar"></div> | |
| </div> | |
| <div class="relative h-8 bg-gray-700 rounded-lg overflow-hidden"> | |
| <div class="absolute inset-0 flex items-center"> | |
| <div class="h-4 w-full bg-gray-600 rounded" id="timelineBg"></div> | |
| </div> | |
| <div class="absolute inset-0 flex items-center"> | |
| <div class="h-4 bg-hf-purple rounded" id="timelineSelection"></div> | |
| </div> | |
| <div class="absolute timeline-handle bg-hf-purple rounded cursor-move" id="startHandle"></div> | |
| <div class="absolute timeline-handle bg-hf-purple rounded cursor-move" id="endHandle"></div> | |
| </div> | |
| </div> | |
| <!-- Crop Controls --> | |
| <div class="mb-6"> | |
| <h3 class="font-medium mb-3">Crop Video</h3> | |
| <div class="flex flex-wrap gap-3 mb-4"> | |
| <button class="bg-gray-700 hover:bg-gray-600 px-4 py-2 rounded-lg flex items-center gap-2 transition crop-btn" data-ratio="original"> | |
| <i class="fas fa-expand"></i> Original | |
| </button> | |
| <button class="bg-gray-700 hover:bg-gray-600 px-4 py-2 rounded-lg flex items-center gap-2 transition crop-btn" data-ratio="16:9"> | |
| <i class="fas fa-expand"></i> 16:9 | |
| </button> | |
| <button class="bg-gray-700 hover:bg-gray-600 px-4 py-2 rounded-lg flex items-center gap-2 transition crop-btn" data-ratio="4:3"> | |
| <i class="fas fa-expand"></i> 4:3 | |
| </button> | |
| <button class="bg-gray-700 hover:bg-gray-600 px-4 py-2 rounded-lg flex items-center gap-2 transition crop-btn" data-ratio="1:1"> | |
| <i class="fas fa-expand"></i> Square (1:1) | |
| </button> | |
| </div> | |
| <div class="hidden" id="cropOverlayContainer"> | |
| <div class="relative border-2 border-hf-purple rounded-lg overflow-hidden" id="cropOverlay"></div> | |
| <div class="flex justify-between mt-2"> | |
| <span class="text-sm text-gray-400">Drag edges to adjust crop</span> | |
| <button id="applyCrop" class="text-sm text-hf-purple hover:text-purple-300">Apply Crop</button> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Trim Action Buttons --> | |
| <div class="flex flex-wrap gap-3 mt-6"> | |
| <button id="setStartBtn" class="bg-hf-purple hover:bg-purple-800 px-4 py-2 rounded-lg flex items-center gap-2 transition"> | |
| <i class="fas fa-flag"></i> Set Start Time Here | |
| </button> | |
| <button id="setEndBtn" class="bg-hf-purple hover:bg-purple-800 px-4 py-2 rounded-lg flex items-center gap-2 transition"> | |
| <i class="fas fa-flag-checkered"></i> Set End Time Here | |
| </button> | |
| </div> | |
| </div> | |
| <!-- Output Preview (hidden by default) --> | |
| <div id="outputPreview" class="bg-gray-800 rounded-xl p-6 hidden"> | |
| <div class="flex justify-between items-center mb-4"> | |
| <h2 class="text-xl font-semibold flex items-center gap-2"> | |
| <i class="fas fa-check-circle"></i> Processed Video Preview | |
| </h2> | |
| <div class="flex gap-2"> | |
| <button id="backToEditBtn" class="bg-gray-700 hover:bg-gray-600 px-4 py-2 rounded-lg flex items-center gap-2 transition"> | |
| <i class="fas fa-edit"></i> Back to Editing | |
| </button> | |
| <button id="startOverBtn" class="bg-gray-700 hover:bg-gray-600 px-4 py-2 rounded-lg flex items-center gap-2 transition"> | |
| <i class="fas fa-redo"></i> Start Over | |
| </button> | |
| </div> | |
| </div> | |
| <div class="mb-4 bg-gray-700 rounded-lg p-3"> | |
| <div class="flex justify-between mb-1"> | |
| <span class="text-gray-400">Trimmed Duration:</span> | |
| <span id="processedDuration" class="font-mono">00:00</span> | |
| </div> | |
| <div class="flex justify-between"> | |
| <span class="text-gray-400">Output Resolution:</span> | |
| <span id="processedResolution" class="font-mono">1920x1080</span> | |
| </div> | |
| </div> | |
| <div class="video-container bg-black rounded-lg overflow-hidden mb-4"> | |
| <video id="outputVideo" class="w-full h-full" controls></video> | |
| </div> | |
| <div class="flex flex-col sm:flex-row gap-3"> | |
| <button id="finalDownloadBtn" disabled class="flex-1 bg-hf-purple hover:bg-purple-800 py-3 rounded-lg font-medium flex items-center justify-center gap-2 transition opacity-50"> | |
| <i class="fas fa-download"></i> Download Now | |
| </button> | |
| <button id="startOverBtn" class="flex-1 bg-gray-700 hover:bg-gray-600 py-3 rounded-lg font-medium flex items-center justify-center gap-2 transition"> | |
| <i class="fas fa-redo"></i> Process Again | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Footer --> | |
| <footer class="mt-12 text-center text-gray-400 text-sm"> | |
| <p>Video Cutter & Trimmer - A HuggingFace Space Project</p> | |
| <p class="mt-1">Supports MP4, AVI, MKV, MOV formats with original quality preservation</p> | |
| </footer> | |
| </div> | |
| <script> | |
| // DOM Elements | |
| const fileInput = document.getElementById('fileInput'); | |
| const dropZone = document.getElementById('dropZone'); | |
| const videoPlayer = document.getElementById('videoPlayer'); | |
| const noVideo = document.getElementById('noVideo'); | |
| const videoContainer = document.getElementById('videoContainer'); | |
| const videoInfo = document.getElementById('videoInfo'); | |
| const fileName = document.getElementById('fileName'); | |
| const fileSize = document.getElementById('fileSize'); | |
| const fileDuration = document.getElementById('fileDuration'); | |
| const fileResolution = document.getElementById('fileResolution'); | |
| const clearFile = document.getElementById('clearFile'); | |
| const processBtn = document.getElementById('processBtn'); | |
| const downloadBtn = document.getElementById('downloadBtn'); | |
| const progressBar = document.getElementById('progressBar'); | |
| const timelineBg = document.getElementById('timelineBg'); | |
| const timelineSelection = document.getElementById('timelineSelection'); | |
| const startHandle = document.getElementById('startHandle'); | |
| const endHandle = document.getElementById('endHandle'); | |
| const currentTime = document.getElementById('currentTime'); | |
| const totalDuration = document.getElementById('totalDuration'); | |
| const startMin = document.getElementById('startMin'); | |
| const startSec = document.getElementById('startSec'); | |
| const endMin = document.getElementById('endMin'); | |
| const endSec = document.getElementById('endSec'); | |
| // Variables | |
| let videoFile = null; | |
| let isDragging = false; | |
| let activeHandle = null; | |
| let videoDuration = 0; | |
| let currentCropRatio = 'original'; | |
| let cropOverlay = null; | |
| let isCropping = false; | |
| // Event Listeners | |
| dropZone.addEventListener('click', () => fileInput.click()); | |
| fileInput.addEventListener('change', handleFileSelect); | |
| dropZone.addEventListener('dragover', handleDragOver); | |
| dropZone.addEventListener('dragleave', handleDragLeave); | |
| dropZone.addEventListener('drop', handleDrop); | |
| clearFile.addEventListener('click', clearVideo); | |
| videoPlayer.addEventListener('timeupdate', updateProgress); | |
| videoPlayer.addEventListener('loadedmetadata', setupVideo); | |
| startHandle.addEventListener('mousedown', startDrag); | |
| endHandle.addEventListener('mousedown', startDrag); | |
| document.addEventListener('mousemove', handleDrag); | |
| document.addEventListener('mouseup', stopDrag); | |
| startMin.addEventListener('change', updateFromTimeInputs); | |
| startSec.addEventListener('change', updateFromTimeInputs); | |
| endMin.addEventListener('change', updateFromTimeInputs); | |
| endSec.addEventListener('change', updateFromTimeInputs); | |
| // New DOM elements | |
| const setStartBtn = document.getElementById('setStartBtn'); | |
| const setEndBtn = document.getElementById('setEndBtn'); | |
| const outputPreview = document.getElementById('outputPreview'); | |
| const outputVideo = document.getElementById('outputVideo'); | |
| const backToEditBtn = document.getElementById('backToEditBtn'); | |
| const finalDownloadBtn = document.getElementById('finalDownloadBtn'); | |
| const startOverBtn = document.getElementById('startOverBtn'); | |
| // Event listeners | |
| setStartBtn.addEventListener('click', () => setTrimPoint('start')); | |
| setEndBtn.addEventListener('click', () => setTrimPoint('end')); | |
| backToEditBtn.addEventListener('click', () => { | |
| outputPreview.classList.add('hidden'); | |
| }); | |
| startOverBtn.addEventListener('click', clearVideo); | |
| finalDownloadBtn.addEventListener('click', downloadProcessedVideo); | |
| processBtn.addEventListener('click', processVideo); | |
| // Crop event listeners | |
| document.querySelectorAll('.crop-btn').forEach(btn => { | |
| btn.addEventListener('click', handleCropSelection); | |
| }); | |
| // Functions | |
| function handleFileSelect(e) { | |
| const file = e.target.files[0]; | |
| if (file && isValidFile(file)) { | |
| loadVideo(file); | |
| } | |
| } | |
| function handleDragOver(e) { | |
| e.preventDefault(); | |
| dropZone.classList.add('border-hf-purple'); | |
| } | |
| function handleDragLeave() { | |
| dropZone.classList.remove('border-hf-purple'); | |
| } | |
| function handleDrop(e) { | |
| e.preventDefault(); | |
| dropZone.classList.remove('border-hf-purple'); | |
| const file = e.dataTransfer.files[0]; | |
| if (file && isValidFile(file)) { | |
| loadVideo(file); | |
| } | |
| } | |
| function isValidFile(file) { | |
| const validTypes = ['video/mp4', 'video/quicktime', 'video/x-msvideo', 'video/x-matroska']; | |
| const validExtensions = ['.mp4', '.avi', '.mkv', '.mov']; | |
| const extension = file.name.substring(file.name.lastIndexOf('.')).toLowerCase(); | |
| return validTypes.includes(file.type) || validExtensions.includes(extension); | |
| } | |
| function loadVideo(file) { | |
| videoFile = file; | |
| const url = URL.createObjectURL(file); | |
| videoPlayer.src = url; | |
| // Show video info | |
| fileName.textContent = file.name; | |
| fileSize.textContent = formatFileSize(file.size); | |
| // Show video elements | |
| noVideo.classList.add('hidden'); | |
| videoInfo.classList.remove('hidden'); | |
| processBtn.disabled = false; | |
| processBtn.classList.remove('opacity-50'); | |
| // Wait for metadata to be loaded | |
| if (videoPlayer.readyState > 0) { | |
| setupVideo(); | |
| } | |
| } | |
| function setupVideo() { | |
| videoDuration = videoPlayer.duration; | |
| fileDuration.textContent = formatTime(videoDuration); | |
| totalDuration.textContent = formatTime(videoDuration); | |
| // Set end time inputs | |
| const endMinutes = Math.floor(videoDuration / 60); | |
| const endSeconds = Math.floor(videoDuration % 60); | |
| endMin.value = endMinutes; | |
| endSec.value = endSeconds; | |
| // Setup timeline | |
| setupTimeline(); | |
| } | |
| function setupTimeline() { | |
| timelineBg.style.width = '100%'; | |
| // Position handles | |
| startHandle.style.left = '0%'; | |
| endHandle.style.left = '100%'; | |
| // Initial selection | |
| updateSelection(); | |
| } | |
| function updateSelection() { | |
| const startPos = parseFloat(startHandle.style.left) / 100; | |
| const endPos = parseFloat(endHandle.style.left) / 100; | |
| timelineSelection.style.left = `${startPos * 100}%`; | |
| timelineSelection.style.width = `${(endPos - startPos) * 100}%`; | |
| // Update time inputs | |
| const startTime = startPos * videoDuration; | |
| const endTime = endPos * videoDuration; | |
| const startMinutes = Math.floor(startTime / 60); | |
| const startSeconds = Math.floor(startTime % 60); | |
| startMin.value = startMinutes; | |
| startSec.value = startSeconds; | |
| const endMinutes = Math.floor(endTime / 60); | |
| const endSeconds = Math.floor(endTime % 60); | |
| endMin.value = endMinutes; | |
| endSec.value = endSeconds; | |
| } | |
| function updateFromTimeInputs() { | |
| const startTime = (parseInt(startMin.value) || 0) * 60 + (parseInt(startSec.value) || 0); | |
| const endTime = (parseInt(endMin.value) || 0) * 60 + (parseInt(endSec.value) || 0); | |
| // Validate times | |
| if (startTime < 0) startTime = 0; | |
| if (endTime > videoDuration) endTime = videoDuration; | |
| if (startTime > endTime) startTime = endTime; | |
| // Update handles | |
| startHandle.style.left = `${(startTime / videoDuration) * 100}%`; | |
| endHandle.style.left = `${(endTime / videoDuration) * 100}%`; | |
| updateSelection(); | |
| // Seek video if playing | |
| if (!videoPlayer.paused) { | |
| videoPlayer.currentTime = startTime; | |
| } | |
| } | |
| function startDrag(e) { | |
| isDragging = true; | |
| activeHandle = e.target; | |
| e.preventDefault(); | |
| } | |
| function handleDrag(e) { | |
| if (!isDragging) return; | |
| const rect = timelineBg.getBoundingClientRect(); | |
| let pos = (e.clientX - rect.left) / rect.width; | |
| // Constrain position | |
| pos = Math.max(0, Math.min(1, pos)); | |
| if (activeHandle === startHandle) { | |
| const endPos = parseFloat(endHandle.style.left) / 100; | |
| pos = Math.min(pos, endPos - 0.01); // Small offset to prevent overlap | |
| activeHandle.style.left = `${pos * 100}%`; | |
| } else if (activeHandle === endHandle) { | |
| const startPos = parseFloat(startHandle.style.left) / 100; | |
| pos = Math.max(pos, startPos + 0.01); // Small offset to prevent overlap | |
| activeHandle.style.left = `${pos * 100}%`; | |
| } | |
| updateSelection(); | |
| } | |
| function stopDrag() { | |
| isDragging = false; | |
| activeHandle = null; | |
| } | |
| function setTrimPoint(type) { | |
| const currentTime = videoPlayer.currentTime; | |
| const percentage = (currentTime / videoDuration) * 100; | |
| if (type === 'start') { | |
| startHandle.style.left = `${percentage}%`; | |
| startMin.value = Math.floor(currentTime / 60); | |
| startSec.value = Math.floor(currentTime % 60); | |
| } else { | |
| endHandle.style.left = `${percentage}%`; | |
| endMin.value = Math.floor(currentTime / 60); | |
| endSec.value = Math.floor(currentTime % 60); | |
| } | |
| updateSelection(); | |
| } | |
| // Connect process button | |
| processBtn.addEventListener('click', processVideo); | |
| // Processing state variables | |
| let processingInterval; | |
| let processingProgress = 0; | |
| const processingMessages = [ | |
| "Preparing video for processing...", | |
| "Analyzing video metadata...", | |
| "Trimming video segment (${startTime}s to ${endTime}s)...", | |
| "Applying crop settings (${currentCropRatio})...", | |
| "Encoding video output...", | |
| "Finalizing processed video..." | |
| ]; | |
| async function processVideo() { | |
| if (!videoFile) { | |
| alert('Please upload a video first'); | |
| return; | |
| } | |
| // Check for browser support | |
| if (typeof SharedArrayBuffer === 'undefined') { | |
| alert('Your browser doesn\'t support required features for video processing. Please try a modern browser like Chrome or Firefox.'); | |
| return; | |
| } | |
| // Show processing overlay | |
| const processingOverlay = document.createElement('div'); | |
| processingOverlay.className = 'fixed inset-0 bg-black bg-opacity-75 flex flex-col items-center justify-center z-50'; | |
| processingOverlay.innerHTML = ` | |
| <div class="bg-gray-800 rounded-xl p-8 max-w-md w-full"> | |
| <h3 class="text-xl font-semibold mb-4">Processing Video</h3> | |
| <div class="mb-2 text-sm" id="processingMessage">Initializing FFmpeg...</div> | |
| <div class="w-full bg-gray-700 rounded-full h-2.5 mb-4"> | |
| <div id="processingBar" class="bg-hf-purple h-2.5 rounded-full" style="width: 0%"></div> | |
| </div> | |
| <div class="flex justify-between text-xs text-gray-400"> | |
| <span id="processingPercent">0%</span> | |
| <span id="processingTime">Estimated time: calculating...</span> | |
| </div> | |
| </div> | |
| `; | |
| document.body.appendChild(processingOverlay); | |
| try { | |
| const { createFFmpeg, fetchFile } = FFmpeg; | |
| const ffmpeg = createFFmpeg({ | |
| log: true, | |
| corePath: 'https://unpkg.com/@ffmpeg/core@0.11.0/dist/ffmpeg-core.js', | |
| progress: ({ ratio }) => { | |
| const percent = Math.round(ratio * 100); | |
| document.getElementById('processingBar').style.width = `${percent}%`; | |
| document.getElementById('processingPercent').textContent = `${percent}%`; | |
| document.getElementById('processingMessage').textContent = | |
| percent < 30 ? "Loading FFmpeg..." : | |
| percent < 60 ? "Processing video..." : | |
| "Finalizing output..."; | |
| } | |
| }); | |
| try { | |
| await ffmpeg.load(); | |
| } catch (error) { | |
| console.error('FFmpeg load error:', error); | |
| processingOverlay.remove(); | |
| alert('Error: Your browser doesn\'t support required features for video processing. Please try a modern browser like Chrome or Firefox.'); | |
| return; | |
| } | |
| // Get trim times | |
| const startTime = (parseInt(startMin.value) || 0) * 60 + (parseInt(startSec.value) || 0); | |
| const endTime = (parseInt(endMin.value) || 0) * 60 + (parseInt(endSec.value) || 0); | |
| const duration = endTime - startTime; | |
| // Read input file | |
| ffmpeg.FS('writeFile', 'input.mp4', await fetchFile(videoFile)); | |
| // Build FFmpeg command | |
| let command = [ | |
| '-i', 'input.mp4', | |
| '-ss', startTime.toString(), | |
| '-t', duration.toString() | |
| ]; | |
| // Add crop if needed | |
| if (currentCropRatio !== 'original') { | |
| const [w, h] = currentCropRatio.split(':').map(Number); | |
| command.push('-vf', `crop=in_w*${w}/${w+1}:in_h*${h}/${h+1}`); | |
| } | |
| command.push('-c:v', 'libx264', '-preset', 'fast', '-c:a', 'copy', 'output.mp4'); | |
| try { | |
| // Run FFmpeg | |
| await ffmpeg.run(...command); | |
| // Get output | |
| const data = ffmpeg.FS('readFile', 'output.mp4'); | |
| const blob = new Blob([data.buffer], { type: 'video/mp4' }); | |
| processedVideoBlob = blob; | |
| const url = URL.createObjectURL(blob); | |
| // Show processed video | |
| processingOverlay.remove(); | |
| showProcessedVideo(url, duration); | |
| } catch (error) { | |
| console.error('FFmpeg processing error:', error); | |
| processingOverlay.remove(); | |
| alert('Error processing video. Please try a different video or browser.'); | |
| return; | |
| } | |
| } catch (error) { | |
| console.error('Error processing video:', error); | |
| processingOverlay.remove(); | |
| alert('Error processing video: ' + error.message); | |
| } | |
| } | |
| let processedVideoBlob = null; | |
| function showProcessedVideo(url, duration) { | |
| outputPreview.classList.remove('hidden'); | |
| // Create preview with actual processed video | |
| const previewContainer = document.getElementById('outputVideo').parentNode; | |
| previewContainer.innerHTML = ` | |
| <div class="relative w-full h-full"> | |
| <video id="outputVideo" class="w-full h-full" controls playsinline> | |
| <source src="${url}" type="video/mp4"> | |
| </video> | |
| </div> | |
| `; | |
| // Show trimmed duration | |
| document.getElementById('processedDuration').textContent = formatTime(duration); | |
| // Enable download button | |
| finalDownloadBtn.disabled = false; | |
| finalDownloadBtn.classList.remove('opacity-50'); | |
| // Scroll to output preview | |
| outputPreview.scrollIntoView({ behavior: 'smooth' }); | |
| } | |
| function downloadProcessedVideo() { | |
| if (!processedVideoBlob) return; | |
| const url = URL.createObjectURL(processedVideoBlob); | |
| const a = document.createElement('a'); | |
| a.href = url; | |
| a.download = `edited_${videoFile.name}`; | |
| document.body.appendChild(a); | |
| a.click(); | |
| setTimeout(() => { | |
| document.body.removeChild(a); | |
| URL.revokeObjectURL(url); | |
| }, 100); | |
| // Show download complete message | |
| const toast = document.createElement('div'); | |
| toast.className = 'fixed bottom-4 right-4 bg-green-600 text-white px-4 py-2 rounded-lg shadow-lg'; | |
| toast.textContent = 'Download started!'; | |
| document.body.appendChild(toast); | |
| setTimeout(() => toast.remove(), 3000); | |
| } | |
| function updateProgress() { | |
| const currentTimeValue = videoPlayer.currentTime; | |
| const duration = videoPlayer.duration; | |
| // Update progress bar | |
| progressBar.style.width = `${(currentTimeValue / duration) * 100}%`; | |
| // Update current time display | |
| currentTime.textContent = formatTime(currentTimeValue); | |
| // Check if current time is outside selection | |
| const startPos = parseFloat(startHandle.style.left) / 100; | |
| const endPos = parseFloat(endHandle.style.left) / 100; | |
| if (currentTimeValue < startPos * duration || currentTimeValue > endPos * duration) { | |
| videoPlayer.pause(); | |
| videoPlayer.currentTime = startPos * duration; | |
| } | |
| } | |
| function clearVideo() { | |
| videoPlayer.src = ''; | |
| videoFile = null; | |
| noVideo.classList.remove('hidden'); | |
| videoInfo.classList.add('hidden'); | |
| processBtn.disabled = true; | |
| processBtn.classList.add('opacity-50'); | |
| downloadBtn.disabled = true; | |
| downloadBtn.classList.add('opacity-50'); | |
| fileInput.value = ''; | |
| // Reset timeline | |
| progressBar.style.width = '0%'; | |
| currentTime.textContent = '00:00'; | |
| totalDuration.textContent = '00:00'; | |
| timelineSelection.style.width = '0%'; | |
| } | |
| function formatTime(seconds) { | |
| const mins = Math.floor(seconds / 60); | |
| const secs = Math.floor(seconds % 60); | |
| return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`; | |
| } | |
| function formatFileSize(bytes) { | |
| if (bytes === 0) return '0 Bytes'; | |
| const k = 1024; | |
| const sizes = ['Bytes', 'KB', 'MB', 'GB']; | |
| const i = Math.floor(Math.log(bytes) / Math.log(k)); | |
| return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; | |
| } | |
| function handleCropSelection(e) { | |
| const ratio = e.target.dataset.ratio; | |
| currentCropRatio = ratio; | |
| // Highlight selected button | |
| document.querySelectorAll('.crop-btn').forEach(btn => { | |
| btn.classList.remove('bg-hf-purple'); | |
| btn.classList.add('bg-gray-700'); | |
| }); | |
| e.target.classList.remove('bg-gray-700'); | |
| e.target.classList.add('bg-hf-purple'); | |
| if (ratio === 'original') { | |
| document.getElementById('cropOverlayContainer').classList.add('hidden'); | |
| videoPlayer.style.objectFit = 'contain'; | |
| return; | |
| } | |
| // Show crop overlay | |
| document.getElementById('cropOverlayContainer').classList.remove('hidden'); | |
| const container = document.getElementById('cropOverlay'); | |
| container.innerHTML = ''; | |
| // Create crop overlay based on ratio | |
| const [w, h] = ratio.split(':').map(Number); | |
| const aspectRatio = w / h; | |
| // Create full-size overlay | |
| cropOverlay = document.createElement('div'); | |
| cropOverlay.className = 'absolute inset-0 bg-black bg-opacity-50'; | |
| // Calculate and apply the crop mask | |
| const containerWidth = container.offsetWidth; | |
| const containerHeight = container.offsetHeight; | |
| const containerAspect = containerWidth / containerHeight; | |
| if (containerAspect > aspectRatio) { | |
| // Container is wider than target aspect - crop sides | |
| const newWidth = containerHeight * aspectRatio; | |
| const sideMargin = (containerWidth - newWidth) / 2; | |
| cropOverlay.style.left = `${sideMargin}px`; | |
| cropOverlay.style.right = `${sideMargin}px`; | |
| } else { | |
| // Container is taller than target aspect - crop top/bottom | |
| const newHeight = containerWidth / aspectRatio; | |
| const vertMargin = (containerHeight - newHeight) / 2; | |
| cropOverlay.style.top = `${vertMargin}px`; | |
| cropOverlay.style.bottom = `${vertMargin}px`; | |
| } | |
| container.appendChild(cropOverlay); | |
| // Store crop settings for processing | |
| console.log('Auto-crop applied with ratio:', ratio); | |
| } | |
| </script> | |
| <p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - 🧬 <a href="https://enzostvs-deepsite.hf.space?remix=beatbox1200/vidcuttrim" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body> | |
| </html> |