All checks were successful
Build Images and Deploy / Update-PROD-Stack (push) Successful in 1m12s
1596 lines
58 KiB
HTML
1596 lines
58 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>Simple Video Editor</title>
|
||
<style>
|
||
* {
|
||
margin: 0;
|
||
padding: 0;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
body {
|
||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||
min-height: 100vh;
|
||
padding: 20px;
|
||
}
|
||
|
||
.container {
|
||
max-width: 1200px;
|
||
margin: 0 auto;
|
||
background: white;
|
||
border-radius: 12px;
|
||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
|
||
overflow: hidden;
|
||
}
|
||
|
||
.header {
|
||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||
color: white;
|
||
padding: 30px;
|
||
text-align: center;
|
||
}
|
||
|
||
.header h1 {
|
||
font-size: 2.5em;
|
||
margin-bottom: 10px;
|
||
}
|
||
|
||
.header p {
|
||
opacity: 0.9;
|
||
font-size: 1.1em;
|
||
}
|
||
|
||
.content {
|
||
padding: 40px;
|
||
}
|
||
|
||
.section {
|
||
margin-bottom: 30px;
|
||
padding: 20px;
|
||
background: #f8f9fa;
|
||
border-radius: 8px;
|
||
border: 2px solid #e9ecef;
|
||
}
|
||
|
||
.section.hidden {
|
||
display: none;
|
||
}
|
||
|
||
.section h2 {
|
||
color: #667eea;
|
||
margin-bottom: 20px;
|
||
font-size: 1.5em;
|
||
}
|
||
|
||
.upload-area {
|
||
border: 3px dashed #667eea;
|
||
border-radius: 8px;
|
||
padding: 40px;
|
||
text-align: center;
|
||
background: white;
|
||
cursor: pointer;
|
||
transition: all 0.3s;
|
||
}
|
||
|
||
.upload-area:hover {
|
||
background: #f8f9fa;
|
||
border-color: #764ba2;
|
||
}
|
||
|
||
.upload-area.dragover {
|
||
background: #e7f3ff;
|
||
border-color: #667eea;
|
||
}
|
||
|
||
.upload-icon {
|
||
font-size: 3em;
|
||
margin-bottom: 15px;
|
||
}
|
||
|
||
input[type="file"] {
|
||
display: none;
|
||
}
|
||
|
||
.btn {
|
||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||
color: white;
|
||
border: none;
|
||
padding: 12px 30px;
|
||
border-radius: 6px;
|
||
font-size: 1em;
|
||
cursor: pointer;
|
||
transition: transform 0.2s;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.btn:hover {
|
||
transform: translateY(-2px);
|
||
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
|
||
}
|
||
|
||
.btn:disabled {
|
||
opacity: 0.5;
|
||
cursor: not-allowed;
|
||
transform: none;
|
||
}
|
||
|
||
.btn-secondary {
|
||
background: #6c757d;
|
||
}
|
||
|
||
.video-preview {
|
||
margin: 20px auto;
|
||
text-align: center;
|
||
position: relative;
|
||
display: flex;
|
||
justify-content: center;
|
||
}
|
||
|
||
.video-wrapper {
|
||
position: relative;
|
||
display: inline-block;
|
||
}
|
||
|
||
.video-preview video {
|
||
max-width: 100%;
|
||
max-height: 500px;
|
||
border-radius: 8px;
|
||
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
|
||
display: block;
|
||
}
|
||
|
||
.video-controls {
|
||
margin: 15px 0;
|
||
padding: 15px;
|
||
background: white;
|
||
border-radius: 8px;
|
||
text-align: center;
|
||
}
|
||
|
||
.crop-canvas {
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
cursor: crosshair;
|
||
border-radius: 8px;
|
||
display: none;
|
||
}
|
||
|
||
.crop-canvas.active {
|
||
display: block;
|
||
}
|
||
|
||
.crop-mode-btn {
|
||
padding: 10px 20px;
|
||
background: #28a745;
|
||
color: white;
|
||
border: none;
|
||
border-radius: 6px;
|
||
cursor: pointer;
|
||
font-weight: 600;
|
||
transition: all 0.3s;
|
||
}
|
||
|
||
.crop-mode-btn:hover {
|
||
background: #218838;
|
||
}
|
||
|
||
.crop-mode-btn.active {
|
||
background: #dc3545;
|
||
}
|
||
|
||
.crop-hint {
|
||
color: #28a745;
|
||
font-size: 0.9em;
|
||
margin: 5px 0;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.trim-buttons {
|
||
display: flex;
|
||
gap: 10px;
|
||
margin: 15px 0;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.trim-btn {
|
||
flex: 1;
|
||
min-width: 200px;
|
||
padding: 12px 20px;
|
||
background: #007bff;
|
||
color: white;
|
||
border: none;
|
||
border-radius: 6px;
|
||
cursor: pointer;
|
||
font-weight: 600;
|
||
transition: all 0.3s;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 8px;
|
||
}
|
||
|
||
.trim-btn:hover {
|
||
background: #0056b3;
|
||
transform: translateY(-1px);
|
||
}
|
||
|
||
.trim-btn:active {
|
||
transform: translateY(0);
|
||
}
|
||
|
||
.quick-actions {
|
||
display: flex;
|
||
gap: 10px;
|
||
margin: 15px 0;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.quick-action-btn {
|
||
flex: 1;
|
||
min-width: 150px;
|
||
padding: 10px 16px;
|
||
background: white;
|
||
color: #667eea;
|
||
border: 2px solid #667eea;
|
||
border-radius: 6px;
|
||
cursor: pointer;
|
||
font-weight: 600;
|
||
transition: all 0.3s;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 6px;
|
||
}
|
||
|
||
.quick-action-btn:hover {
|
||
background: #667eea;
|
||
color: white;
|
||
transform: translateY(-1px);
|
||
}
|
||
|
||
.quick-action-btn.active {
|
||
background: #667eea;
|
||
color: white;
|
||
}
|
||
|
||
.current-time-display {
|
||
background: white;
|
||
padding: 10px 15px;
|
||
border-radius: 6px;
|
||
text-align: center;
|
||
font-size: 1.1em;
|
||
color: #667eea;
|
||
font-weight: 600;
|
||
border: 2px solid #e9ecef;
|
||
}
|
||
|
||
.scale-slider-container {
|
||
margin: 15px 0;
|
||
}
|
||
|
||
.scale-slider {
|
||
width: 100%;
|
||
height: 8px;
|
||
border-radius: 4px;
|
||
background: #e9ecef;
|
||
outline: none;
|
||
-webkit-appearance: none;
|
||
}
|
||
|
||
.scale-slider::-webkit-slider-thumb {
|
||
-webkit-appearance: none;
|
||
appearance: none;
|
||
width: 20px;
|
||
height: 20px;
|
||
border-radius: 50%;
|
||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||
cursor: pointer;
|
||
border: 2px solid white;
|
||
box-shadow: 0 2px 5px rgba(0,0,0,0.2);
|
||
}
|
||
|
||
.scale-slider::-moz-range-thumb {
|
||
width: 20px;
|
||
height: 20px;
|
||
border-radius: 50%;
|
||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||
cursor: pointer;
|
||
border: 2px solid white;
|
||
box-shadow: 0 2px 5px rgba(0,0,0,0.2);
|
||
}
|
||
|
||
.scale-display {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 10px;
|
||
}
|
||
|
||
.scale-value {
|
||
font-size: 1.2em;
|
||
font-weight: 600;
|
||
color: #667eea;
|
||
}
|
||
|
||
.resolution-preview {
|
||
background: white;
|
||
padding: 10px;
|
||
border-radius: 6px;
|
||
margin-top: 10px;
|
||
font-size: 0.9em;
|
||
color: #495057;
|
||
}
|
||
|
||
.video-info {
|
||
background: white;
|
||
padding: 15px;
|
||
border-radius: 8px;
|
||
margin: 15px 0;
|
||
}
|
||
|
||
.video-info p {
|
||
margin: 5px 0;
|
||
color: #495057;
|
||
}
|
||
|
||
.form-group {
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.form-group label {
|
||
display: block;
|
||
margin-bottom: 8px;
|
||
color: #495057;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.form-group input,
|
||
.form-group select {
|
||
width: 100%;
|
||
padding: 10px;
|
||
border: 2px solid #e9ecef;
|
||
border-radius: 6px;
|
||
font-size: 1em;
|
||
transition: border-color 0.3s;
|
||
}
|
||
|
||
.form-group input:focus,
|
||
.form-group select:focus {
|
||
outline: none;
|
||
border-color: #667eea;
|
||
}
|
||
|
||
.form-row {
|
||
display: grid;
|
||
grid-template-columns: 1fr 1fr;
|
||
gap: 15px;
|
||
}
|
||
|
||
.progress-container {
|
||
margin: 20px 0;
|
||
display: none;
|
||
}
|
||
|
||
.progress-bar {
|
||
width: 100%;
|
||
height: 30px;
|
||
background: #e9ecef;
|
||
border-radius: 15px;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.progress-fill {
|
||
height: 100%;
|
||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||
transition: width 0.3s;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
color: white;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.alert {
|
||
padding: 15px;
|
||
border-radius: 6px;
|
||
margin: 15px 0;
|
||
}
|
||
|
||
.alert-success {
|
||
background: #d4edda;
|
||
color: #155724;
|
||
border: 1px solid #c3e6cb;
|
||
}
|
||
|
||
.alert-error {
|
||
background: #f8d7da;
|
||
color: #721c24;
|
||
border: 1px solid #f5c6cb;
|
||
}
|
||
|
||
.range-container {
|
||
margin: 20px 0;
|
||
}
|
||
|
||
.range-slider {
|
||
width: 100%;
|
||
}
|
||
|
||
.crop-controls {
|
||
background: white;
|
||
padding: 15px;
|
||
border-radius: 8px;
|
||
margin-top: 15px;
|
||
}
|
||
|
||
.preset-resolutions {
|
||
display: flex;
|
||
gap: 10px;
|
||
flex-wrap: wrap;
|
||
margin-bottom: 15px;
|
||
}
|
||
|
||
.preset-btn {
|
||
padding: 8px 16px;
|
||
background: white;
|
||
border: 2px solid #667eea;
|
||
color: #667eea;
|
||
border-radius: 6px;
|
||
cursor: pointer;
|
||
transition: all 0.3s;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.preset-btn:hover,
|
||
.preset-btn.active {
|
||
background: #667eea;
|
||
color: white;
|
||
}
|
||
|
||
.button-group {
|
||
display: flex;
|
||
gap: 15px;
|
||
margin-top: 20px;
|
||
flex-wrap: wrap;
|
||
justify-content: center;
|
||
}
|
||
|
||
.loader {
|
||
border: 4px solid #f3f3f3;
|
||
border-top: 4px solid #667eea;
|
||
border-radius: 50%;
|
||
width: 40px;
|
||
height: 40px;
|
||
animation: spin 1s linear infinite;
|
||
margin: 20px auto;
|
||
display: none;
|
||
}
|
||
|
||
@keyframes spin {
|
||
0% { transform: rotate(0deg); }
|
||
100% { transform: rotate(360deg); }
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="container">
|
||
<div class="header">
|
||
<h1>🎬 Simple Video Editor</h1>
|
||
<p>Trim, crop, and convert your videos with ease</p>
|
||
</div>
|
||
|
||
<div class="content">
|
||
<!-- Step 1: Upload Video -->
|
||
<div class="section" id="upload-section">
|
||
<h2>1. Select Video</h2>
|
||
<div class="upload-area" id="upload-area">
|
||
<div class="upload-icon">📹</div>
|
||
<h3>Drag and drop your video here</h3>
|
||
<p>or click to browse</p>
|
||
<p style="margin-top: 10px; color: #6c757d; font-size: 0.9em;">
|
||
Supported formats: MP4, AVI, MOV, MKV, WMV, FLV, WebM
|
||
</p>
|
||
<input type="file" id="video-input" accept="video/*">
|
||
</div>
|
||
<div class="loader" id="upload-loader"></div>
|
||
</div>
|
||
|
||
<!-- Step 2: Edit Video -->
|
||
<div class="section hidden" id="edit-section">
|
||
<h2>2. Edit Video</h2>
|
||
|
||
<div class="video-preview">
|
||
<div class="video-wrapper">
|
||
<video id="video-preview" controls></video>
|
||
<canvas id="crop-canvas" class="crop-canvas"></canvas>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="video-info" id="video-info"></div>
|
||
|
||
<!-- Trim Controls -->
|
||
<div class="form-group">
|
||
<label>⏱️ Trim Video</label>
|
||
<div class="current-time-display">
|
||
Video Position: <span id="current-time-display">0:00</span>
|
||
</div>
|
||
<div class="trim-buttons">
|
||
<button class="trim-btn" id="set-start-btn">
|
||
⏮️ Set Start Here
|
||
</button>
|
||
<button class="trim-btn" id="set-end-btn">
|
||
⏭️ Set End Here
|
||
</button>
|
||
</div>
|
||
<p style="font-size: 0.9em; color: #6c757d; text-align: center; margin: 10px 0;">
|
||
💡 Play the video and click the buttons to set trim points at the current playback position
|
||
</p>
|
||
<div class="form-row">
|
||
<div>
|
||
<label style="font-weight: normal;">Start Time (seconds)</label>
|
||
<input type="number" id="start-time" value="0" min="0" step="0.1">
|
||
</div>
|
||
<div>
|
||
<label style="font-weight: normal;">End Time (seconds)</label>
|
||
<input type="number" id="end-time" min="0" step="0.1">
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Crop Controls -->
|
||
<div class="form-group">
|
||
<label>✂️ Crop Video</label>
|
||
<p style="font-size: 0.9em; color: #6c757d; margin-bottom: 10px;">
|
||
💡 Draw crop area on video above • Double-click edges or corners to snap
|
||
</p>
|
||
<div class="crop-controls">
|
||
<div class="form-row">
|
||
<div>
|
||
<label style="font-weight: normal;">X Position</label>
|
||
<input type="number" id="crop-x" value="0" min="0">
|
||
</div>
|
||
<div>
|
||
<label style="font-weight: normal;">Y Position</label>
|
||
<input type="number" id="crop-y" value="0" min="0">
|
||
</div>
|
||
</div>
|
||
<div class="form-row" style="margin-top: 10px;">
|
||
<div>
|
||
<label style="font-weight: normal;">Crop Width</label>
|
||
<input type="number" id="crop-width" placeholder="Full width" min="0">
|
||
</div>
|
||
<div>
|
||
<label style="font-weight: normal;">Crop Height</label>
|
||
<input type="number" id="crop-height" placeholder="Full height" min="0">
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Scale Controls -->
|
||
<div class="form-group">
|
||
<label>📐 Output Scale</label>
|
||
<div class="scale-slider-container">
|
||
<div class="scale-display">
|
||
<span>Scale: <span class="scale-value" id="scale-value-display">100%</span></span>
|
||
<span id="resolution-info" style="color: #6c757d;">Output: <strong id="output-resolution">--</strong></span>
|
||
</div>
|
||
<input type="range" class="scale-slider" id="scale-slider" min="10" max="100" value="100" step="5">
|
||
<div class="resolution-preview" id="resolution-preview">
|
||
<p style="margin: 0;">💡 <strong>How it works:</strong> Scale is applied to the cropped area (or original if no crop). At 50%, a 200x400 crop becomes 100x200.</p>
|
||
</div>
|
||
<div class="preset-resolutions" style="margin-top: 15px;">
|
||
<button class="preset-btn" data-scale="100">100% (Original)</button>
|
||
<button class="preset-btn" data-scale="75">75%</button>
|
||
<button class="preset-btn" data-scale="50">50%</button>
|
||
<button class="preset-btn" data-scale="25">25%</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Compression Quality Controls -->
|
||
<div class="form-group">
|
||
<label>💾 Compression Quality</label>
|
||
<div class="scale-slider-container">
|
||
<div class="scale-display">
|
||
<span>Quality: <span class="scale-value" id="quality-value-display">High</span></span>
|
||
<span style="color: #6c757d;">CRF: <strong id="crf-value-display">23</strong></span>
|
||
</div>
|
||
<input type="range" class="scale-slider" id="quality-slider" min="18" max="42" value="23" step="1">
|
||
<div class="resolution-preview">
|
||
<p style="margin: 0;">💡 <strong>Lower CRF</strong> = Better quality, larger file. <strong>Higher CRF</strong> = More compressed, smaller file.</p>
|
||
</div>
|
||
<div class="preset-resolutions" style="margin-top: 15px;">
|
||
<button class="preset-btn" data-quality="18">Overkill</button>
|
||
<button class="preset-btn" data-quality="21">Quality</button>
|
||
<button class="preset-btn" data-quality="23">Balanced</button>
|
||
<button class="preset-btn" data-quality="26">Compressed</button>
|
||
<button class="preset-btn" data-quality="30">Small File</button>
|
||
<button class="preset-btn" data-quality="40">Dog 💩</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Quick Actions -->
|
||
<div class="form-group">
|
||
<label>⚡ Quick Actions</label>
|
||
<div class="quick-actions">
|
||
<button class="quick-action-btn" id="mute-audio-btn" title="Remove audio track from video">
|
||
🔇 Mute Audio
|
||
</button>
|
||
<button class="quick-action-btn" id="rotate-btn" title="Rotate video 90° clockwise">
|
||
🔄 Rotate 90°
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="button-group">
|
||
<button class="btn" id="process-btn">🎬 Process Video</button>
|
||
<button class="btn btn-secondary" id="reset-btn">🔄 Start Over</button>
|
||
</div>
|
||
|
||
<div class="loader" id="process-loader"></div>
|
||
<div id="alert-container"></div>
|
||
</div>
|
||
|
||
<!-- Step 3: Download -->
|
||
<div class="section hidden" id="download-section">
|
||
<h2>3. Download Processed Video</h2>
|
||
<p style="margin-bottom: 20px;">Your video has been processed successfully!</p>
|
||
|
||
<div class="video-preview">
|
||
<div class="video-wrapper">
|
||
<video id="processed-video-preview" controls style="max-width: 100%; max-height: 500px; border-radius: 8px; box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);"></video>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="button-group">
|
||
<button class="btn" id="download-btn">⬇️ Download Video</button>
|
||
<button class="btn btn-secondary" id="back-to-edit-btn">✏️ Make More Changes</button>
|
||
<button class="btn btn-secondary" id="new-video-btn">🎬 Process Another Video</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
let currentFileId = null;
|
||
let videoInfo = null;
|
||
let cropMode = false;
|
||
let cropStartX = 0;
|
||
let cropStartY = 0;
|
||
let cropRect = null;
|
||
let scalePercentage = 100;
|
||
let rotationDegrees = 0;
|
||
let muteAudio = false;
|
||
let compressionQuality = 23;
|
||
let isDraggingHandle = false;
|
||
let isDraggingEdge = false;
|
||
let isDraggingBox = false;
|
||
let activeHandle = null;
|
||
let activeEdge = null;
|
||
let dragOffsetX = 0;
|
||
let dragOffsetY = 0;
|
||
const handleSize = 12;
|
||
|
||
// Upload area handling
|
||
const uploadArea = document.getElementById('upload-area');
|
||
const videoInput = document.getElementById('video-input');
|
||
const uploadLoader = document.getElementById('upload-loader');
|
||
const processLoader = document.getElementById('process-loader');
|
||
const cropCanvas = document.getElementById('crop-canvas');
|
||
const cropCtx = cropCanvas.getContext('2d');
|
||
const videoPreview = document.getElementById('video-preview');
|
||
const currentTimeDisplay = document.getElementById('current-time-display');
|
||
const setStartBtn = document.getElementById('set-start-btn');
|
||
const setEndBtn = document.getElementById('set-end-btn');
|
||
|
||
uploadArea.addEventListener('click', () => videoInput.click());
|
||
|
||
uploadArea.addEventListener('dragover', (e) => {
|
||
e.preventDefault();
|
||
uploadArea.classList.add('dragover');
|
||
});
|
||
|
||
uploadArea.addEventListener('dragleave', () => {
|
||
uploadArea.classList.remove('dragover');
|
||
});
|
||
|
||
uploadArea.addEventListener('drop', (e) => {
|
||
e.preventDefault();
|
||
uploadArea.classList.remove('dragover');
|
||
const files = e.dataTransfer.files;
|
||
if (files.length > 0) {
|
||
handleFileUpload(files[0]);
|
||
}
|
||
});
|
||
|
||
videoInput.addEventListener('change', (e) => {
|
||
if (e.target.files.length > 0) {
|
||
handleFileUpload(e.target.files[0]);
|
||
}
|
||
});
|
||
|
||
async function handleFileUpload(file) {
|
||
const formData = new FormData();
|
||
formData.append('video', file);
|
||
|
||
uploadLoader.style.display = 'block';
|
||
|
||
try {
|
||
const response = await fetch('/upload', {
|
||
method: 'POST',
|
||
body: formData
|
||
});
|
||
|
||
const data = await response.json();
|
||
|
||
if (!response.ok) {
|
||
throw new Error(data.error || 'Upload failed');
|
||
}
|
||
|
||
currentFileId = data.file_id;
|
||
videoInfo = data.info;
|
||
|
||
// Show video preview
|
||
const videoPreview = document.getElementById('video-preview');
|
||
videoPreview.src = URL.createObjectURL(file);
|
||
|
||
// Update video info
|
||
const videoInfoDiv = document.getElementById('video-info');
|
||
videoInfoDiv.innerHTML = `
|
||
<p><strong>File:</strong> ${data.original_name}</p>
|
||
<p><strong>Resolution:</strong> ${videoInfo.width}x${videoInfo.height}</p>
|
||
<p><strong>Duration:</strong> ${videoInfo.duration.toFixed(2)} seconds</p>
|
||
<p><strong>Codec:</strong> ${videoInfo.codec}</p>
|
||
<div style="margin-top: 15px; text-align: center;">
|
||
<button class="crop-mode-btn" id="crop-mode-btn">🎯 Click to Draw Crop Area</button>
|
||
<p class="crop-hint" id="crop-hint" style="display: none;">Click and drag to select • Drag corners and edges to resize or double click them to max • Drag center to move</p>
|
||
</div>
|
||
`;
|
||
|
||
// Set default values
|
||
document.getElementById('end-time').value = videoInfo.duration.toFixed(2);
|
||
scalePercentage = 100;
|
||
scaleSlider.value = 100;
|
||
updateScaleDisplay();
|
||
|
||
// Set max values for crop inputs
|
||
document.getElementById('crop-x').max = videoInfo.width;
|
||
document.getElementById('crop-y').max = videoInfo.height;
|
||
document.getElementById('crop-width').max = videoInfo.width;
|
||
document.getElementById('crop-height').max = videoInfo.height;
|
||
|
||
// Setup canvas for cropping
|
||
setupCropCanvas();
|
||
|
||
// Setup crop mode button (now that it exists)
|
||
setupCropButton();
|
||
|
||
// Show edit section
|
||
document.getElementById('upload-section').classList.add('hidden');
|
||
document.getElementById('edit-section').classList.remove('hidden');
|
||
|
||
} catch (error) {
|
||
alert('Error uploading video: ' + error.message);
|
||
} finally {
|
||
uploadLoader.style.display = 'none';
|
||
}
|
||
}
|
||
|
||
// Setup crop canvas
|
||
function setupCropCanvas() {
|
||
const video = document.getElementById('video-preview');
|
||
|
||
// Wait for video to load metadata
|
||
video.addEventListener('loadedmetadata', () => {
|
||
resizeCropCanvas();
|
||
});
|
||
|
||
// Resize canvas when window resizes
|
||
window.addEventListener('resize', resizeCropCanvas);
|
||
}
|
||
|
||
function resizeCropCanvas() {
|
||
const video = document.getElementById('video-preview');
|
||
cropCanvas.width = video.offsetWidth;
|
||
cropCanvas.height = video.offsetHeight;
|
||
}
|
||
|
||
// Setup crop button event listener
|
||
function setupCropButton() {
|
||
const cropModeBtn = document.getElementById('crop-mode-btn');
|
||
const cropHint = document.getElementById('crop-hint');
|
||
|
||
cropModeBtn.addEventListener('click', () => {
|
||
cropMode = !cropMode;
|
||
if (cropMode) {
|
||
cropCanvas.classList.add('active');
|
||
cropModeBtn.classList.add('active');
|
||
cropModeBtn.textContent = '✅ Done with Crop';
|
||
cropHint.style.display = 'block';
|
||
document.getElementById('video-preview').pause();
|
||
} else {
|
||
cropCanvas.classList.remove('active');
|
||
cropModeBtn.classList.remove('active');
|
||
cropModeBtn.textContent = '🎯 Click to Draw Crop Area';
|
||
cropHint.style.display = 'none';
|
||
clearCropCanvas();
|
||
cropRect = null;
|
||
}
|
||
});
|
||
}
|
||
|
||
// Canvas drawing for crop selection
|
||
let isDrawing = false;
|
||
let startX, startY;
|
||
|
||
cropCanvas.addEventListener('mousedown', (e) => {
|
||
if (!cropMode) return;
|
||
e.preventDefault();
|
||
const rect = cropCanvas.getBoundingClientRect();
|
||
const mouseX = e.clientX - rect.left;
|
||
const mouseY = e.clientY - rect.top;
|
||
|
||
handlePointerDown(mouseX, mouseY);
|
||
});
|
||
|
||
cropCanvas.addEventListener('touchstart', (e) => {
|
||
if (!cropMode) return;
|
||
e.preventDefault();
|
||
const rect = cropCanvas.getBoundingClientRect();
|
||
const touch = e.touches[0];
|
||
const mouseX = touch.clientX - rect.left;
|
||
const mouseY = touch.clientY - rect.top;
|
||
|
||
handlePointerDown(mouseX, mouseY);
|
||
});
|
||
|
||
function handlePointerDown(mouseX, mouseY) {
|
||
// Check if clicking on a handle
|
||
if (cropRect) {
|
||
const handle = getHandleAtPosition(mouseX, mouseY);
|
||
if (handle) {
|
||
isDraggingHandle = true;
|
||
activeHandle = handle;
|
||
return;
|
||
}
|
||
|
||
// Check if clicking on an edge
|
||
const edge = getEdgeAtPosition(mouseX, mouseY);
|
||
if (edge) {
|
||
isDraggingEdge = true;
|
||
activeEdge = edge;
|
||
return;
|
||
}
|
||
|
||
// Check if clicking inside the box to move it
|
||
if (isInsideRect(mouseX, mouseY)) {
|
||
isDraggingBox = true;
|
||
dragOffsetX = mouseX - cropRect.x;
|
||
dragOffsetY = mouseY - cropRect.y;
|
||
cropCanvas.style.cursor = 'move';
|
||
return;
|
||
}
|
||
}
|
||
|
||
// Start new selection
|
||
isDrawing = true;
|
||
cropRect = null;
|
||
startX = mouseX;
|
||
startY = mouseY;
|
||
}
|
||
|
||
cropCanvas.addEventListener('mousemove', (e) => {
|
||
if (!cropMode) return;
|
||
const rect = cropCanvas.getBoundingClientRect();
|
||
const mouseX = e.clientX - rect.left;
|
||
const mouseY = e.clientY - rect.top;
|
||
|
||
handlePointerMove(mouseX, mouseY);
|
||
});
|
||
|
||
cropCanvas.addEventListener('touchmove', (e) => {
|
||
if (!cropMode) return;
|
||
e.preventDefault();
|
||
const rect = cropCanvas.getBoundingClientRect();
|
||
const touch = e.touches[0];
|
||
const mouseX = touch.clientX - rect.left;
|
||
const mouseY = touch.clientY - rect.top;
|
||
|
||
handlePointerMove(mouseX, mouseY);
|
||
});
|
||
|
||
function handlePointerMove(mouseX, mouseY) {
|
||
// Update cursor based on position
|
||
if (!isDrawing && !isDraggingHandle && !isDraggingEdge && !isDraggingBox && cropRect) {
|
||
const handle = getHandleAtPosition(mouseX, mouseY);
|
||
if (handle) {
|
||
cropCanvas.style.cursor = getHandleCursor(handle);
|
||
} else {
|
||
const edge = getEdgeAtPosition(mouseX, mouseY);
|
||
if (edge) {
|
||
cropCanvas.style.cursor = getEdgeCursor(edge);
|
||
} else if (isInsideRect(mouseX, mouseY)) {
|
||
cropCanvas.style.cursor = 'move';
|
||
} else {
|
||
cropCanvas.style.cursor = 'crosshair';
|
||
}
|
||
}
|
||
}
|
||
|
||
// Handle resizing
|
||
if (isDraggingHandle && cropRect) {
|
||
resizeCropRect(mouseX, mouseY);
|
||
updateFormFields();
|
||
return;
|
||
}
|
||
|
||
// Handle edge resizing
|
||
if (isDraggingEdge && cropRect) {
|
||
resizeEdge(mouseX, mouseY);
|
||
updateFormFields();
|
||
return;
|
||
}
|
||
|
||
// Handle moving
|
||
if (isDraggingBox && cropRect) {
|
||
moveCropRect(mouseX, mouseY);
|
||
updateFormFields();
|
||
return;
|
||
}
|
||
|
||
// Handle initial drawing
|
||
if (isDrawing) {
|
||
const width = mouseX - startX;
|
||
const height = mouseY - startY;
|
||
cropRect = {
|
||
x: Math.min(startX, mouseX),
|
||
y: Math.min(startY, mouseY),
|
||
width: Math.abs(width),
|
||
height: Math.abs(height)
|
||
};
|
||
drawCropRect();
|
||
}
|
||
}
|
||
|
||
cropCanvas.addEventListener('mouseup', (e) => {
|
||
if (!cropMode) return;
|
||
handlePointerUp();
|
||
});
|
||
|
||
cropCanvas.addEventListener('touchend', (e) => {
|
||
if (!cropMode) return;
|
||
e.preventDefault();
|
||
handlePointerUp();
|
||
});
|
||
|
||
function handlePointerUp() {
|
||
if (isDrawing || isDraggingHandle || isDraggingEdge || isDraggingBox) {
|
||
updateFormFields();
|
||
}
|
||
|
||
isDrawing = false;
|
||
isDraggingHandle = false;
|
||
isDraggingEdge = false;
|
||
isDraggingBox = false;
|
||
activeHandle = null;
|
||
activeEdge = null;
|
||
cropCanvas.style.cursor = 'crosshair';
|
||
|
||
// Keep crop mode active and canvas visible
|
||
drawCropRect();
|
||
}
|
||
|
||
// Handle mouse leaving canvas while dragging
|
||
cropCanvas.addEventListener('mouseleave', (e) => {
|
||
if (isDrawing || isDraggingHandle || isDraggingEdge || isDraggingBox) {
|
||
handlePointerUp();
|
||
}
|
||
});
|
||
|
||
cropCanvas.addEventListener('touchcancel', (e) => {
|
||
if (cropMode) {
|
||
handlePointerUp();
|
||
}
|
||
});
|
||
|
||
// Double-click on handles to snap to edges
|
||
cropCanvas.addEventListener('dblclick', (e) => {
|
||
if (!cropMode || !cropRect) return;
|
||
|
||
const rect = cropCanvas.getBoundingClientRect();
|
||
const mouseX = e.clientX - rect.left;
|
||
const mouseY = e.clientY - rect.top;
|
||
|
||
// Check for edge click first (between corners)
|
||
const edge = getEdgeAtPosition(mouseX, mouseY);
|
||
if (edge) {
|
||
snapEdgeToSide(edge);
|
||
updateFormFields();
|
||
drawCropRect();
|
||
return;
|
||
}
|
||
|
||
// Fall back to corner handling
|
||
const handle = getHandleAtPosition(mouseX, mouseY);
|
||
if (handle) {
|
||
snapHandleToEdge(handle);
|
||
updateFormFields();
|
||
drawCropRect();
|
||
}
|
||
});
|
||
|
||
function drawCropRect() {
|
||
clearCropCanvas();
|
||
|
||
if (!cropRect) return;
|
||
|
||
const { x, y, width, height } = cropRect;
|
||
|
||
// Draw semi-transparent overlay
|
||
cropCtx.fillStyle = 'rgba(0, 0, 0, 0.5)';
|
||
cropCtx.fillRect(0, 0, cropCanvas.width, cropCanvas.height);
|
||
|
||
// Clear the selected area
|
||
cropCtx.clearRect(x, y, width, height);
|
||
|
||
// Draw border around selection
|
||
cropCtx.strokeStyle = '#667eea';
|
||
cropCtx.lineWidth = 3;
|
||
cropCtx.strokeRect(x, y, width, height);
|
||
|
||
// Draw resize handles
|
||
drawHandle(x, y); // Top-left
|
||
drawHandle(x + width, y); // Top-right
|
||
drawHandle(x, y + height); // Bottom-left
|
||
drawHandle(x + width, y + height); // Bottom-right
|
||
|
||
// Calculate actual video dimensions for display
|
||
const video = document.getElementById('video-preview');
|
||
const scaleX = videoInfo.width / video.offsetWidth;
|
||
const scaleY = videoInfo.height / video.offsetHeight;
|
||
const actualWidth = Math.abs(Math.round(width * scaleX));
|
||
const actualHeight = Math.abs(Math.round(height * scaleY));
|
||
|
||
// Draw dimensions text (showing actual video pixels)
|
||
cropCtx.fillStyle = '#667eea';
|
||
cropCtx.font = 'bold 14px sans-serif';
|
||
const text = `${actualWidth} × ${actualHeight}`;
|
||
const textY = y > 25 ? y - 10 : y + height + 20;
|
||
cropCtx.fillText(text, x + 5, textY);
|
||
}
|
||
|
||
function drawHandle(x, y) {
|
||
cropCtx.fillStyle = '#ffffff';
|
||
cropCtx.strokeStyle = '#667eea';
|
||
cropCtx.lineWidth = 2;
|
||
const offset = handleSize / 2;
|
||
cropCtx.fillRect(x - offset, y - offset, handleSize, handleSize);
|
||
cropCtx.strokeRect(x - offset, y - offset, handleSize, handleSize);
|
||
}
|
||
|
||
function getHandleAtPosition(x, y) {
|
||
if (!cropRect) return null;
|
||
|
||
// Larger hit area on mobile (touch targets should be at least 44x44px)
|
||
const hitSize = 'ontouchstart' in window ? 22 : handleSize;
|
||
|
||
const handles = [
|
||
{ name: 'tl', x: cropRect.x, y: cropRect.y },
|
||
{ name: 'tr', x: cropRect.x + cropRect.width, y: cropRect.y },
|
||
{ name: 'bl', x: cropRect.x, y: cropRect.y + cropRect.height },
|
||
{ name: 'br', x: cropRect.x + cropRect.width, y: cropRect.y + cropRect.height }
|
||
];
|
||
|
||
for (const handle of handles) {
|
||
if (Math.abs(x - handle.x) <= hitSize && Math.abs(y - handle.y) <= hitSize) {
|
||
return handle.name;
|
||
}
|
||
}
|
||
return null;
|
||
}
|
||
|
||
function getEdgeAtPosition(x, y) {
|
||
if (!cropRect) return null;
|
||
|
||
// Larger tolerance on mobile for easier touch interaction
|
||
const tolerance = 'ontouchstart' in window ? 25 : 15;
|
||
const { x: rx, y: ry, width: rw, height: rh } = cropRect;
|
||
const hitSize = 'ontouchstart' in window ? 22 : handleSize;
|
||
|
||
// Check if within the rectangle bounds (with tolerance)
|
||
const inHorizontalRange = x >= rx - tolerance && x <= rx + rw + tolerance;
|
||
const inVerticalRange = y >= ry - tolerance && y <= ry + rh + tolerance;
|
||
|
||
// Top edge (exclude corners)
|
||
if (Math.abs(y - ry) <= tolerance && x > rx + hitSize && x < rx + rw - hitSize) {
|
||
return 'top';
|
||
}
|
||
|
||
// Bottom edge (exclude corners)
|
||
if (Math.abs(y - (ry + rh)) <= tolerance && x > rx + hitSize && x < rx + rw - hitSize) {
|
||
return 'bottom';
|
||
}
|
||
|
||
// Left edge (exclude corners)
|
||
if (Math.abs(x - rx) <= tolerance && y > ry + hitSize && y < ry + rh - hitSize) {
|
||
return 'left';
|
||
}
|
||
|
||
// Right edge (exclude corners)
|
||
if (Math.abs(x - (rx + rw)) <= tolerance && y > ry + hitSize && y < ry + rh - hitSize) {
|
||
return 'right';
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
function getHandleCursor(handle) {
|
||
const cursors = {
|
||
'tl': 'nw-resize',
|
||
'tr': 'ne-resize',
|
||
'bl': 'sw-resize',
|
||
'br': 'se-resize'
|
||
};
|
||
return cursors[handle] || 'default';
|
||
}
|
||
|
||
function getEdgeCursor(edge) {
|
||
const cursors = {
|
||
'top': 'ns-resize',
|
||
'bottom': 'ns-resize',
|
||
'left': 'ew-resize',
|
||
'right': 'ew-resize'
|
||
};
|
||
return cursors[edge] || 'default';
|
||
}
|
||
|
||
function isInsideRect(x, y) {
|
||
if (!cropRect) return false;
|
||
return x >= cropRect.x && x <= cropRect.x + cropRect.width &&
|
||
y >= cropRect.y && y <= cropRect.y + cropRect.height;
|
||
}
|
||
|
||
function resizeCropRect(mouseX, mouseY) {
|
||
const minSize = 20;
|
||
|
||
switch (activeHandle) {
|
||
case 'tl':
|
||
const newWidth = cropRect.x + cropRect.width - mouseX;
|
||
const newHeight = cropRect.y + cropRect.height - mouseY;
|
||
if (newWidth > minSize) {
|
||
cropRect.width = newWidth;
|
||
cropRect.x = mouseX;
|
||
}
|
||
if (newHeight > minSize) {
|
||
cropRect.height = newHeight;
|
||
cropRect.y = mouseY;
|
||
}
|
||
break;
|
||
case 'tr':
|
||
const widthTR = mouseX - cropRect.x;
|
||
const heightTR = cropRect.y + cropRect.height - mouseY;
|
||
if (widthTR > minSize) cropRect.width = widthTR;
|
||
if (heightTR > minSize) {
|
||
cropRect.height = heightTR;
|
||
cropRect.y = mouseY;
|
||
}
|
||
break;
|
||
case 'bl':
|
||
const widthBL = cropRect.x + cropRect.width - mouseX;
|
||
const heightBL = mouseY - cropRect.y;
|
||
if (widthBL > minSize) {
|
||
cropRect.width = widthBL;
|
||
cropRect.x = mouseX;
|
||
}
|
||
if (heightBL > minSize) cropRect.height = heightBL;
|
||
break;
|
||
case 'br':
|
||
const widthBR = mouseX - cropRect.x;
|
||
const heightBR = mouseY - cropRect.y;
|
||
if (widthBR > minSize) cropRect.width = widthBR;
|
||
if (heightBR > minSize) cropRect.height = heightBR;
|
||
break;
|
||
}
|
||
|
||
// Constrain to canvas bounds
|
||
cropRect.x = Math.max(0, Math.min(cropRect.x, cropCanvas.width - cropRect.width));
|
||
cropRect.y = Math.max(0, Math.min(cropRect.y, cropCanvas.height - cropRect.height));
|
||
cropRect.width = Math.min(cropRect.width, cropCanvas.width - cropRect.x);
|
||
cropRect.height = Math.min(cropRect.height, cropCanvas.height - cropRect.y);
|
||
|
||
drawCropRect();
|
||
}
|
||
|
||
function resizeEdge(mouseX, mouseY) {
|
||
const minSize = 20;
|
||
|
||
switch (activeEdge) {
|
||
case 'top':
|
||
const newHeight = cropRect.y + cropRect.height - mouseY;
|
||
if (newHeight > minSize && mouseY >= 0) {
|
||
cropRect.height = newHeight;
|
||
cropRect.y = mouseY;
|
||
}
|
||
break;
|
||
case 'bottom':
|
||
const heightBottom = mouseY - cropRect.y;
|
||
if (heightBottom > minSize && mouseY <= cropCanvas.height) {
|
||
cropRect.height = heightBottom;
|
||
}
|
||
break;
|
||
case 'left':
|
||
const newWidth = cropRect.x + cropRect.width - mouseX;
|
||
if (newWidth > minSize && mouseX >= 0) {
|
||
cropRect.width = newWidth;
|
||
cropRect.x = mouseX;
|
||
}
|
||
break;
|
||
case 'right':
|
||
const widthRight = mouseX - cropRect.x;
|
||
if (widthRight > minSize && mouseX <= cropCanvas.width) {
|
||
cropRect.width = widthRight;
|
||
}
|
||
break;
|
||
}
|
||
|
||
// Constrain to canvas bounds
|
||
cropRect.x = Math.max(0, cropRect.x);
|
||
cropRect.y = Math.max(0, cropRect.y);
|
||
cropRect.width = Math.min(cropRect.width, cropCanvas.width - cropRect.x);
|
||
cropRect.height = Math.min(cropRect.height, cropCanvas.height - cropRect.y);
|
||
|
||
drawCropRect();
|
||
}
|
||
|
||
function moveCropRect(mouseX, mouseY) {
|
||
const newX = mouseX - dragOffsetX;
|
||
const newY = mouseY - dragOffsetY;
|
||
|
||
// Constrain to canvas bounds
|
||
cropRect.x = Math.max(0, Math.min(newX, cropCanvas.width - cropRect.width));
|
||
cropRect.y = Math.max(0, Math.min(newY, cropCanvas.height - cropRect.height));
|
||
|
||
drawCropRect();
|
||
}
|
||
|
||
function snapHandleToEdge(handle) {
|
||
if (!cropRect) return;
|
||
|
||
switch (handle) {
|
||
case 'tl': // Top-left -> snap to top-left corner
|
||
cropRect.width += cropRect.x; // Extend width by current x
|
||
cropRect.height += cropRect.y; // Extend height by current y
|
||
cropRect.x = 0;
|
||
cropRect.y = 0;
|
||
break;
|
||
case 'tr': // Top-right -> snap to top-right corner
|
||
cropRect.width = cropCanvas.width - cropRect.x;
|
||
cropRect.height += cropRect.y;
|
||
cropRect.y = 0;
|
||
break;
|
||
case 'bl': // Bottom-left -> snap to bottom-left corner
|
||
cropRect.width += cropRect.x;
|
||
cropRect.height = cropCanvas.height - cropRect.y;
|
||
cropRect.x = 0;
|
||
break;
|
||
case 'br': // Bottom-right -> snap to bottom-right corner
|
||
cropRect.width = cropCanvas.width - cropRect.x;
|
||
cropRect.height = cropCanvas.height - cropRect.y;
|
||
break;
|
||
}
|
||
}
|
||
|
||
function snapEdgeToSide(edge) {
|
||
if (!cropRect) return;
|
||
|
||
switch (edge) {
|
||
case 'top': // Snap top edge to y=0
|
||
cropRect.height += cropRect.y; // Extend height by current y
|
||
cropRect.y = 0;
|
||
break;
|
||
case 'bottom': // Snap bottom edge to canvas height
|
||
cropRect.height = cropCanvas.height - cropRect.y;
|
||
break;
|
||
case 'left': // Snap left edge to x=0
|
||
cropRect.width += cropRect.x; // Extend width by current x
|
||
cropRect.x = 0;
|
||
break;
|
||
case 'right': // Snap right edge to canvas width
|
||
cropRect.width = cropCanvas.width - cropRect.x;
|
||
break;
|
||
}
|
||
}
|
||
|
||
function updateFormFields() {
|
||
if (!cropRect) return;
|
||
|
||
// Calculate actual video coordinates
|
||
const video = document.getElementById('video-preview');
|
||
const scaleX = videoInfo.width / video.offsetWidth;
|
||
const scaleY = videoInfo.height / video.offsetHeight;
|
||
|
||
const actualX = Math.round(cropRect.x * scaleX);
|
||
const actualY = Math.round(cropRect.y * scaleY);
|
||
const actualWidth = Math.round(cropRect.width * scaleX);
|
||
const actualHeight = Math.round(cropRect.height * scaleY);
|
||
|
||
// Update form fields
|
||
document.getElementById('crop-x').value = actualX;
|
||
document.getElementById('crop-y').value = actualY;
|
||
document.getElementById('crop-width').value = actualWidth;
|
||
document.getElementById('crop-height').value = actualHeight;
|
||
|
||
// Update scale display since crop changed
|
||
updateScaleDisplay();
|
||
}
|
||
|
||
function clearCropCanvas() {
|
||
cropCtx.clearRect(0, 0, cropCanvas.width, cropCanvas.height);
|
||
}
|
||
|
||
// Update current time display
|
||
videoPreview.addEventListener('timeupdate', () => {
|
||
const currentTime = videoPreview.currentTime;
|
||
const minutes = Math.floor(currentTime / 60);
|
||
const seconds = Math.floor(currentTime % 60);
|
||
const milliseconds = Math.floor((currentTime % 1) * 10);
|
||
currentTimeDisplay.textContent = `${minutes}:${seconds.toString().padStart(2, '0')}.${milliseconds}`;
|
||
});
|
||
|
||
// Quick actions
|
||
document.getElementById('mute-audio-btn').addEventListener('click', function() {
|
||
muteAudio = !muteAudio;
|
||
if (muteAudio) {
|
||
this.classList.add('active');
|
||
this.textContent = '🔇 Audio Muted';
|
||
} else {
|
||
this.classList.remove('active');
|
||
this.textContent = '🔇 Mute Audio';
|
||
}
|
||
});
|
||
|
||
document.getElementById('rotate-btn').addEventListener('click', function() {
|
||
rotationDegrees = (rotationDegrees + 90) % 360;
|
||
if (rotationDegrees === 0) {
|
||
this.classList.remove('active');
|
||
this.textContent = '🔄 Rotate 90°';
|
||
} else {
|
||
this.classList.add('active');
|
||
this.textContent = `🔄 Rotated ${rotationDegrees}°`;
|
||
}
|
||
});
|
||
|
||
// Set start time button
|
||
setStartBtn.addEventListener('click', () => {
|
||
const currentTime = videoPreview.currentTime;
|
||
document.getElementById('start-time').value = currentTime.toFixed(1);
|
||
|
||
// Flash button feedback
|
||
setStartBtn.style.background = '#28a745';
|
||
setTimeout(() => {
|
||
setStartBtn.style.background = '#007bff';
|
||
}, 200);
|
||
});
|
||
|
||
// Set end time button
|
||
setEndBtn.addEventListener('click', () => {
|
||
const currentTime = videoPreview.currentTime;
|
||
document.getElementById('end-time').value = currentTime.toFixed(1);
|
||
|
||
// Flash button feedback
|
||
setEndBtn.style.background = '#28a745';
|
||
setTimeout(() => {
|
||
setEndBtn.style.background = '#007bff';
|
||
}, 200);
|
||
});
|
||
|
||
// Scale slider
|
||
const scaleSlider = document.getElementById('scale-slider');
|
||
const scaleValueDisplay = document.getElementById('scale-value-display');
|
||
const outputResolution = document.getElementById('output-resolution');
|
||
const qualitySlider = document.getElementById('quality-slider');
|
||
const qualityValueDisplay = document.getElementById('quality-value-display');
|
||
const crfValueDisplay = document.getElementById('crf-value-display');
|
||
|
||
function updateScaleDisplay() {
|
||
scalePercentage = parseInt(scaleSlider.value);
|
||
scaleValueDisplay.textContent = scalePercentage + '%';
|
||
|
||
// Calculate output resolution
|
||
const cropWidth = parseInt(document.getElementById('crop-width').value) || videoInfo.width;
|
||
const cropHeight = parseInt(document.getElementById('crop-height').value) || videoInfo.height;
|
||
|
||
// Round to nearest even number for H.264 compatibility
|
||
const outputWidth = Math.round((cropWidth * scalePercentage) / 100 / 2) * 2;
|
||
const outputHeight = Math.round((cropHeight * scalePercentage) / 100 / 2) * 2;
|
||
|
||
outputResolution.textContent = `${outputWidth}x${outputHeight}`;
|
||
}
|
||
|
||
scaleSlider.addEventListener('input', updateScaleDisplay);
|
||
|
||
// Quality slider
|
||
function updateQualityDisplay() {
|
||
compressionQuality = parseInt(qualitySlider.value);
|
||
crfValueDisplay.textContent = compressionQuality;
|
||
|
||
// Update quality label
|
||
let qualityLabel = 'Balanced';
|
||
if (compressionQuality <= 19) qualityLabel = 'Overkill';
|
||
else if (compressionQuality <= 22) qualityLabel = 'Quality';
|
||
else if (compressionQuality <= 25) qualityLabel = 'Balanced';
|
||
else if (compressionQuality <= 28) qualityLabel = 'Compressed';
|
||
else if (compressionQuality <= 32) qualityLabel = 'Small File';
|
||
else qualityLabel = 'Dog 💩';
|
||
|
||
qualityValueDisplay.textContent = qualityLabel;
|
||
}
|
||
|
||
qualitySlider.addEventListener('input', updateQualityDisplay);
|
||
updateQualityDisplay();
|
||
|
||
// Scale presets
|
||
document.querySelectorAll('.preset-btn').forEach(btn => {
|
||
btn.addEventListener('click', () => {
|
||
const scale = btn.dataset.scale;
|
||
const quality = btn.dataset.quality;
|
||
|
||
if (scale) {
|
||
scaleSlider.value = scale;
|
||
updateScaleDisplay();
|
||
}
|
||
|
||
if (quality) {
|
||
qualitySlider.value = quality;
|
||
updateQualityDisplay();
|
||
}
|
||
});
|
||
});
|
||
|
||
// Update scale display when crop values change
|
||
['crop-width', 'crop-height'].forEach(id => {
|
||
document.getElementById(id).addEventListener('input', updateScaleDisplay);
|
||
});
|
||
|
||
// Process video
|
||
document.getElementById('process-btn').addEventListener('click', async () => {
|
||
const startTime = parseFloat(document.getElementById('start-time').value) || 0;
|
||
const endTime = parseFloat(document.getElementById('end-time').value) || videoInfo.duration;
|
||
const cropX = parseInt(document.getElementById('crop-x').value) || 0;
|
||
const cropY = parseInt(document.getElementById('crop-y').value) || 0;
|
||
const cropWidth = parseInt(document.getElementById('crop-width').value) || 0;
|
||
const cropHeight = parseInt(document.getElementById('crop-height').value) || 0;
|
||
|
||
const payload = {
|
||
file_id: currentFileId,
|
||
start_time: startTime,
|
||
end_time: endTime,
|
||
scale_percentage: scalePercentage,
|
||
rotation: rotationDegrees,
|
||
mute_audio: muteAudio,
|
||
quality: compressionQuality
|
||
};
|
||
|
||
if (cropWidth > 0 && cropHeight > 0) {
|
||
payload.crop = {
|
||
x: cropX,
|
||
y: cropY,
|
||
width: cropWidth,
|
||
height: cropHeight
|
||
};
|
||
}
|
||
|
||
processLoader.style.display = 'block';
|
||
document.getElementById('process-btn').disabled = true;
|
||
|
||
try {
|
||
const response = await fetch('/process', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json'
|
||
},
|
||
body: JSON.stringify(payload)
|
||
});
|
||
|
||
const data = await response.json();
|
||
|
||
if (!response.ok) {
|
||
throw new Error(data.error || 'Processing failed');
|
||
}
|
||
|
||
// Load processed video preview with cache-busting
|
||
const processedVideoPreview = document.getElementById('processed-video-preview');
|
||
processedVideoPreview.src = `/preview/${currentFileId}?t=${Date.now()}`;
|
||
|
||
// Show download section
|
||
document.getElementById('edit-section').classList.add('hidden');
|
||
document.getElementById('download-section').classList.remove('hidden');
|
||
|
||
} catch (error) {
|
||
const alertContainer = document.getElementById('alert-container');
|
||
alertContainer.innerHTML = `<div class="alert alert-error">Error: ${error.message}</div>`;
|
||
} finally {
|
||
processLoader.style.display = 'none';
|
||
document.getElementById('process-btn').disabled = false;
|
||
}
|
||
});
|
||
|
||
// Download video
|
||
document.getElementById('download-btn').addEventListener('click', () => {
|
||
window.location.href = `/download/${currentFileId}`;
|
||
});
|
||
|
||
// Back to edit button
|
||
document.getElementById('back-to-edit-btn').addEventListener('click', backToEdit);
|
||
|
||
// Reset buttons
|
||
document.getElementById('reset-btn').addEventListener('click', resetApp);
|
||
document.getElementById('new-video-btn').addEventListener('click', resetApp);
|
||
|
||
function backToEdit() {
|
||
// Go back to edit section without resetting values
|
||
document.getElementById('download-section').classList.add('hidden');
|
||
document.getElementById('edit-section').classList.remove('hidden');
|
||
|
||
// Clear the processed video preview to free memory
|
||
const processedVideoPreview = document.getElementById('processed-video-preview');
|
||
processedVideoPreview.src = '';
|
||
}
|
||
|
||
async function resetApp() {
|
||
if (currentFileId) {
|
||
await fetch(`/cleanup/${currentFileId}`, { method: 'DELETE' });
|
||
}
|
||
|
||
currentFileId = null;
|
||
videoInfo = null;
|
||
cropMode = false;
|
||
cropRect = null;
|
||
rotationDegrees = 0;
|
||
muteAudio = false;
|
||
scalePercentage = 100;
|
||
compressionQuality = 23;
|
||
|
||
videoPreview.src = '';;
|
||
videoInput.value = '';
|
||
clearCropCanvas();
|
||
cropCanvas.classList.remove('active');
|
||
|
||
// Reset all form inputs
|
||
document.getElementById('start-time').value = '0';
|
||
document.getElementById('end-time').value = '0';
|
||
document.getElementById('crop-x').value = '0';
|
||
document.getElementById('crop-y').value = '0';
|
||
document.getElementById('crop-width').value = '';
|
||
document.getElementById('crop-height').value = '';
|
||
document.getElementById('scale-slider').value = '100';
|
||
document.getElementById('scale-value-display').textContent = '100%';
|
||
document.getElementById('output-resolution').textContent = '--';
|
||
document.getElementById('current-time-display').textContent = '0:00';
|
||
document.getElementById('quality-slider').value = '23';
|
||
updateQualityDisplay();
|
||
|
||
// Reset quick actions
|
||
document.getElementById('mute-audio-btn').classList.remove('active');
|
||
document.getElementById('mute-audio-btn').textContent = '🔇 Mute Audio';
|
||
document.getElementById('rotate-btn').classList.remove('active');
|
||
document.getElementById('rotate-btn').textContent = '🔄 Rotate 90°';
|
||
|
||
// Clear any alerts
|
||
document.getElementById('alert-container').innerHTML = '';
|
||
|
||
document.getElementById('upload-section').classList.remove('hidden');
|
||
document.getElementById('edit-section').classList.add('hidden');
|
||
document.getElementById('download-section').classList.add('hidden');
|
||
}
|
||
</script>
|
||
</body>
|
||
</html>
|