percent based resolution
All checks were successful
Build Images and Deploy / Update-PROD-Stack (push) Successful in 1m9s
All checks were successful
Build Images and Deploy / Update-PROD-Stack (push) Successful in 1m9s
This commit is contained in:
13
app.py
13
app.py
@@ -124,13 +124,12 @@ def process_video():
|
|||||||
if width and height:
|
if width and height:
|
||||||
filters.append(f"crop={width}:{height}:{x}:{y}")
|
filters.append(f"crop={width}:{height}:{x}:{y}")
|
||||||
|
|
||||||
# Add scale filter if resolution specified
|
# Add scale filter if scale percentage specified (not 100%)
|
||||||
resolution = data.get('resolution')
|
scale_percentage = data.get('scale_percentage', 100)
|
||||||
if resolution:
|
if scale_percentage != 100:
|
||||||
width = resolution.get('width')
|
# Scale based on percentage - ffmpeg will apply this after crop
|
||||||
height = resolution.get('height')
|
scale_filter = f"scale=iw*{scale_percentage/100}:ih*{scale_percentage/100}"
|
||||||
if width and height:
|
filters.append(scale_filter)
|
||||||
filters.append(f"scale={width}:{height}")
|
|
||||||
|
|
||||||
# Apply filters if any
|
# Apply filters if any
|
||||||
if filters:
|
if filters:
|
||||||
|
|||||||
@@ -184,6 +184,63 @@
|
|||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
|
||||||
.timeline-container {
|
.timeline-container {
|
||||||
margin: 15px 0;
|
margin: 15px 0;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
@@ -513,24 +570,23 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Resolution Controls -->
|
<!-- Scale Controls -->
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>📐 Output Resolution</label>
|
<label>📐 Output Scale</label>
|
||||||
<div class="preset-resolutions">
|
<div class="scale-slider-container">
|
||||||
<button class="preset-btn" data-width="3840" data-height="2160">4K (3840x2160)</button>
|
<div class="scale-display">
|
||||||
<button class="preset-btn" data-width="1920" data-height="1080">1080p (1920x1080)</button>
|
<span>Scale: <span class="scale-value" id="scale-value-display">100%</span></span>
|
||||||
<button class="preset-btn" data-width="1280" data-height="720">720p (1280x720)</button>
|
<span id="resolution-info" style="color: #6c757d;">Output: <strong id="output-resolution">--</strong></span>
|
||||||
<button class="preset-btn" data-width="854" data-height="480">480p (854x480)</button>
|
|
||||||
<button class="preset-btn active" data-width="0" data-height="0">Original</button>
|
|
||||||
</div>
|
|
||||||
<div class="form-row">
|
|
||||||
<div>
|
|
||||||
<label style="font-weight: normal;">Width</label>
|
|
||||||
<input type="number" id="width" placeholder="Original" min="0">
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<input type="range" class="scale-slider" id="scale-slider" min="10" max="100" value="100" step="5">
|
||||||
<label style="font-weight: normal;">Height</label>
|
<div class="resolution-preview" id="resolution-preview">
|
||||||
<input type="number" id="height" placeholder="Original" min="0">
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -595,6 +651,7 @@
|
|||||||
let isDraggingEnd = false;
|
let isDraggingEnd = false;
|
||||||
let timelineStartPos = 0;
|
let timelineStartPos = 0;
|
||||||
let timelineEndPos = 100;
|
let timelineEndPos = 100;
|
||||||
|
let scalePercentage = 100;
|
||||||
|
|
||||||
// Upload area handling
|
// Upload area handling
|
||||||
const uploadArea = document.getElementById('upload-area');
|
const uploadArea = document.getElementById('upload-area');
|
||||||
@@ -674,6 +731,9 @@
|
|||||||
|
|
||||||
// Set default values
|
// Set default values
|
||||||
document.getElementById('end-time').value = videoInfo.duration.toFixed(2);
|
document.getElementById('end-time').value = videoInfo.duration.toFixed(2);
|
||||||
|
scalePercentage = 100;
|
||||||
|
scaleSlider.value = 100;
|
||||||
|
updateScaleDisplay();
|
||||||
|
|
||||||
// Setup canvas for cropping
|
// Setup canvas for cropping
|
||||||
setupCropCanvas();
|
setupCropCanvas();
|
||||||
@@ -915,26 +975,47 @@
|
|||||||
updateTimeline();
|
updateTimeline();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Resolution presets
|
// Scale slider
|
||||||
|
const scaleSlider = document.getElementById('scale-slider');
|
||||||
|
const scaleValueDisplay = document.getElementById('scale-value-display');
|
||||||
|
const outputResolution = document.getElementById('output-resolution');
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
const outputWidth = Math.round((cropWidth * scalePercentage) / 100);
|
||||||
|
const outputHeight = Math.round((cropHeight * scalePercentage) / 100);
|
||||||
|
|
||||||
|
outputResolution.textContent = `${outputWidth}x${outputHeight}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
scaleSlider.addEventListener('input', updateScaleDisplay);
|
||||||
|
|
||||||
|
// Scale presets
|
||||||
document.querySelectorAll('.preset-btn').forEach(btn => {
|
document.querySelectorAll('.preset-btn').forEach(btn => {
|
||||||
btn.addEventListener('click', () => {
|
btn.addEventListener('click', () => {
|
||||||
document.querySelectorAll('.preset-btn').forEach(b => b.classList.remove('active'));
|
const scale = btn.dataset.scale;
|
||||||
btn.classList.add('active');
|
if (scale) {
|
||||||
|
scaleSlider.value = scale;
|
||||||
const width = btn.dataset.width;
|
updateScaleDisplay();
|
||||||
const height = btn.dataset.height;
|
}
|
||||||
|
|
||||||
document.getElementById('width').value = width === '0' ? '' : width;
|
|
||||||
document.getElementById('height').value = height === '0' ? '' : height;
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Update scale display when crop values change
|
||||||
|
['crop-width', 'crop-height'].forEach(id => {
|
||||||
|
document.getElementById(id).addEventListener('input', updateScaleDisplay);
|
||||||
|
});
|
||||||
|
|
||||||
// Process video
|
// Process video
|
||||||
document.getElementById('process-btn').addEventListener('click', async () => {
|
document.getElementById('process-btn').addEventListener('click', async () => {
|
||||||
const startTime = parseFloat(document.getElementById('start-time').value) || 0;
|
const startTime = parseFloat(document.getElementById('start-time').value) || 0;
|
||||||
const endTime = parseFloat(document.getElementById('end-time').value) || videoInfo.duration;
|
const endTime = parseFloat(document.getElementById('end-time').value) || videoInfo.duration;
|
||||||
const width = parseInt(document.getElementById('width').value) || 0;
|
|
||||||
const height = parseInt(document.getElementById('height').value) || 0;
|
|
||||||
const cropX = parseInt(document.getElementById('crop-x').value) || 0;
|
const cropX = parseInt(document.getElementById('crop-x').value) || 0;
|
||||||
const cropY = parseInt(document.getElementById('crop-y').value) || 0;
|
const cropY = parseInt(document.getElementById('crop-y').value) || 0;
|
||||||
const cropWidth = parseInt(document.getElementById('crop-width').value) || 0;
|
const cropWidth = parseInt(document.getElementById('crop-width').value) || 0;
|
||||||
@@ -943,13 +1024,10 @@
|
|||||||
const payload = {
|
const payload = {
|
||||||
file_id: currentFileId,
|
file_id: currentFileId,
|
||||||
start_time: startTime,
|
start_time: startTime,
|
||||||
end_time: endTime
|
end_time: endTime,
|
||||||
|
scale_percentage: scalePercentage
|
||||||
};
|
};
|
||||||
|
|
||||||
if (width > 0 && height > 0) {
|
|
||||||
payload.resolution = { width, height };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cropWidth > 0 && cropHeight > 0) {
|
if (cropWidth > 0 && cropHeight > 0) {
|
||||||
payload.crop = {
|
payload.crop = {
|
||||||
x: cropX,
|
x: cropX,
|
||||||
|
|||||||
Reference in New Issue
Block a user