anycoder-a564b169 / index.html
yat343's picture
Upload folder using huggingface_hub
b6133c0 verified
Raw
History Blame Contribute Delete
20.9 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Pro Video Converter | FFmpeg WASM</title>
<script src="https://unpkg.com/@ffmpeg/ffmpeg@0.12.7/dist/umd/ffmpeg.js"></script>
<script src="https://unpkg.com/@ffmpeg/util@0.12.1/dist/umd/index.js"></script>
<style>
:root {
--bg-dark: #0f172a;
--bg-panel: #1e293b;
--primary: #3b82f6;
--primary-hover: #2563eb;
--text-main: #f1f5f9;
--text-muted: #94a3b8;
--border: #334155;
--success: #10b981;
--danger: #ef4444;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
font-family: 'Segoe UI', system-ui, -apple-system, sans-serif;
}
body {
background-color: var(--bg-dark);
color: var(--text-main);
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
padding: 20px;
}
/* Header */
header {
width: 100%;
max-width: 1000px;
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 30px;
padding-bottom: 20px;
border-bottom: 1px solid var(--border);
}
h1 {
font-size: 1.5rem;
background: linear-gradient(90deg, #3b82f6, #8b5cf6);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.anycoder-link a {
color: var(--text-muted);
text-decoration: none;
font-size: 0.9rem;
transition: color 0.3s;
}
.anycoder-link a:hover {
color: var(--primary);
}
/* Main Container */
.app-container {
width: 100%;
max-width: 1000px;
display: grid;
grid-template-columns: 1fr;
gap: 20px;
}
/* Card Styles */
.card {
background: var(--bg-panel);
border: 1px solid var(--border);
border-radius: 12px;
padding: 25px;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
}
/* File Upload Zone */
.upload-zone {
border: 2px dashed var(--border);
border-radius: 12px;
padding: 40px;
text-align: center;
cursor: pointer;
transition: all 0.3s ease;
position: relative;
}
.upload-zone:hover, .upload-zone.drag-over {
border-color: var(--primary);
background: rgba(59, 130, 246, 0.1);
}
.file-info {
margin-top: 15px;
font-size: 0.9rem;
color: var(--text-muted);
display: none;
}
#fileInput {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
opacity: 0;
cursor: pointer;
}
/* Controls Section */
.controls-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.settings-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 20px;
}
.control-group {
margin-bottom: 15px;
}
.control-group label {
display: block;
margin-bottom: 8px;
font-size: 0.9rem;
color: var(--text-muted);
}
/* Sliders */
input[type=range] {
width: 100%;
background: transparent;
-webkit-appearance: none;
}
input[type=range]::-webkit-slider-thumb {
-webkit-appearance: none;
height: 16px;
width: 16px;
border-radius: 50%;
background: var(--primary);
cursor: pointer;
margin-top: -6px;
}
input[type=range]::-webkit-slider-runnable-track {
width: 100%;
height: 4px;
cursor: pointer;
background: var(--border);
border-radius: 2px;
}
.slider-value {
float: right;
color: var(--primary);
font-weight: bold;
}
/* Buttons */
.btn {
background: var(--primary);
color: white;
border: none;
padding: 12px 24px;
border-radius: 8px;
cursor: pointer;
font-weight: 600;
transition: background 0.2s;
width: 100%;
font-size: 1rem;
}
.btn:hover {
background: var(--primary-hover);
}
.btn:disabled {
background: var(--border);
cursor: not-allowed;
}
/* Progress & Logs */
.log-container {
height: 200px;
overflow-y: auto;
background: #000;
border-radius: 8px;
padding: 15px;
font-family: 'Consolas', 'Monaco', monospace;
font-size: 0.85rem;
color: #10b981;
border: 1px solid var(--border);
}
.log-entry {
margin-bottom: 4px;
border-bottom: 1px solid #1e293b;
padding-bottom: 2px;
}
.log-warn { color: #eab308; }
.log-error { color: #ef4444; }
.log-info { color: #94a3b8; }
/* Progress Bar */
.progress-wrapper {
margin-bottom: 15px;
display: none;
}
.progress-bar-bg {
width: 100%;
height: 10px;
background: var(--border);
border-radius: 5px;
overflow: hidden;
}
.progress-bar-fill {
height: 100%;
background: var(--success);
width: 0%;
transition: width 0.3s ease;
}
.status-text {
text-align: center;
margin-top: 5px;
font-size: 0.8rem;
color: var(--text-muted);
}
/* Video Result */
#outputVideo {
width: 100%;
margin-top: 15px;
border-radius: 8px;
display: none;
}
.hidden { display: none !important; }
/* Loading Spinner */
.spinner {
border: 3px solid rgba(255,255,255,0.1);
border-left-color: var(--primary);
border-radius: 50%;
width: 20px;
height: 20px;
animation: spin 1s linear infinite;
display: inline-block;
vertical-align: middle;
margin-right: 10px;
}
@keyframes spin { 100% { transform: rotate(360deg); } }
</style>
</head>
<body>
<header>
<h1>FFmpeg Video Processor</h1>
<div class="anycoder-link">
Built with <a href="https://huggingface.co/spaces/akhaliq/anycoder" target="_blank">anycoder</a>
</div>
</header>
<div class="app-container">
<!-- Upload Section -->
<div class="card">
<div class="upload-zone" id="dropZone">
<input type="file" id="fileInput" accept="video/*">
<div class="icon">📁</div>
<h3>Click or Drag Video File Here</h3>
<p style="color: var(--text-muted); font-size: 0.9rem; margin-top: 5px;">Supports MP4, MKV, MOV, AVI</p>
<div class="file-info" id="fileInfo"></div>
</div>
</div>
<!-- Settings Section -->
<div class="card hidden" id="settingsCard">
<div class="controls-header">
<h2>Conversion Settings</h2>
</div>
<div class="settings-grid">
<!-- Resolution Slider -->
<div class="control-group">
<label>
Scale Resolution
<span class="slider-value" id="scaleValue">Original</span>
</label>
<input type="range" id="scaleRange" min="0" max="100" step="10" value="0">
<p style="font-size: 0.75rem; color: var(--text-muted); margin-top: 5px;">
0 = Original Size. Lower values reduce file size significantly.
</p>
</div>
<!-- Compression Quality -->
<div class="control-group">
<label>
Compression Level (CRF)
<span class="slider-value" id="crfValue">23</span>
</label>
<input type="range" id="crfRange" min="18" max="35" step="1" value="23">
<p style="font-size: 0.75rem; color: var(--text-muted); margin-top: 5px;">
Lower = Better Quality (Larger File). Higher = Lower Quality (Smaller File).
</p>
</div>
<!-- Format Selection -->
<div class="control-group">
<label>Output Format</label>
<select id="formatSelect" style="width: 100%; padding: 8px; background: var(--bg-dark); color: white; border: 1px solid var(--border); border-radius: 6px;">
<option value="mp4">MP4 (H.265)</option>
<option value="mp4">MP4 (H.264)</option>
<option value="webm">WebM (VP9)</option>
</select>
</div>
</div>
<div style="margin-top: 20px;">
<button class="btn" id="convertBtn">Start Conversion</button>
</div>
</div>
<!-- Progress & Logs Section -->
<div class="card hidden" id="progressCard">
<div class="progress-wrapper" id="progressWrapper">
<div class="progress-bar-bg">
<div class="progress-bar-fill" id="progressBar"></div>
</div>
<div class="status-text" id="progressText">Initializing...</div>
</div>
<div class="controls-header">
<h3>Conversion Logs</h3>
<button id="clearLogBtn" style="background:transparent; border:none; color:var(--text-muted); cursor:pointer; font-size:0.8rem;">Clear</button>
</div>
<div class="log-container" id="logContainer">
<!-- Logs go here -->
</div>
<div id="resultArea" style="margin-top: 20px; display:none; text-align: center;">
<h3>Conversion Complete!</h3>
<video id="outputVideo" controls></video>
<br><br>
<a id="downloadLink" class="btn" style="text-decoration: none; display: inline-block;" download>Download Converted File</a>
</div>
</div>
</div>
<script>
const { FFmpeg } = FFmpegWASM;
const { fetchFile } = FFmpegUtil;
let ffmpeg = null;
let selectedFile = null;
// DOM Elements
const fileInput = document.getElementById('fileInput');
const dropZone = document.getElementById('dropZone');
const fileInfo = document.getElementById('fileInfo');
const settingsCard = document.getElementById('settingsCard');
const progressCard = document.getElementById('progressCard');
const convertBtn = document.getElementById('convertBtn');
const logContainer = document.getElementById('logContainer');
const progressBar = document.getElementById('progressBar');
const progressText = document.getElementById('progressText');
const scaleRange = document.getElementById('scaleRange');
const scaleValue = document.getElementById('scaleValue');
const crfRange = document.getElementById('crfRange');
const crfValue = document.getElementById('crfValue');
const outputVideo = document.getElementById('outputVideo');
const downloadLink = document.getElementById('downloadLink');
const resultArea = document.getElementById('resultArea');
const progressWrapper = document.getElementById('progressWrapper');
// Initialize FFmpeg
const initFFmpeg = async () => {
try {
ffmpeg = new FFmpeg();
ffmpeg.on('log', ({ message }) => {
addLog(message);
});
ffmpeg.on('progress', ({ progress, time }) => {
updateProgress(progress, time);
});
// Loading the core from a CDN
// Note: Multithreading requires SharedArrayBuffer (COOP/COEP headers).
// If hosted locally without headers, use single-threaded fallback logic or warn user.
await ffmpeg.load({
coreURL: "https://unpkg.com/@ffmpeg/core@0.12.6/dist/umd/ffmpeg-core.js",
});
addLog("System Ready. Load a video file to begin.");
} catch (e) {
addLog("Error loading FFmpeg: " + e.message, "error");
console.error(e);
}
};
// Initialize immediately
initFFmpeg();
// UI Interaction - File Upload
fileInput.addEventListener('change', (e) => {
if (e.target.files.length > 0) {
handleFile(e.target.files[0]);
}
});
// Drag and Drop
dropZone.addEventListener('dragover', (e) => {
e.preventDefault();
dropZone.classList.add('drag-over');
});
dropZone.addEventListener('dragleave', () => {
dropZone.classList.remove('drag-over');
});
dropZone.addEventListener('drop', (e) => {
e.preventDefault();
dropZone.classList.remove('drag-over');
if (e.dataTransfer.files.length > 0) {
handleFile(e.dataTransfer.files[0]);
}
});
function handleFile(file) {
selectedFile = file;
fileInfo.style.display = 'block';
fileInfo.innerHTML = `Selected: <strong>${file.name}</strong> (${formatBytes(file.size)})`;
settingsCard.classList.remove('hidden');
// Reset UI
resultArea.style.display = 'none';
progressCard.classList.add('hidden');
logContainer.innerHTML = '';
addLog(`File Loaded: ${file.name}`);
}
// UI - Sliders
scaleRange.addEventListener('input', (e) => {
scaleValue.innerText = e.target.value == 0 ? "Original" : e.target.value + "%";
});
crfRange.addEventListener('input', (e) => {
crfValue.innerText = e.target.value;
});
document.getElementById('clearLogBtn').addEventListener('click', () => {
logContainer.innerHTML = '';
});
// Conversion Logic
convertBtn.addEventListener('click', async () => {
if (!ffmpeg || !ffmpeg.loaded) {
alert("FFmpeg is still loading or failed to load. Please refresh.");
return;
}
if (!selectedFile) return;
// Lock UI
convertBtn.disabled = true;
convertBtn.innerHTML = '<span class="spinner"></span> Processing...';
progressCard.classList.remove('hidden');
resultArea.style.display = 'none';
progressWrapper.style.display = 'block';
progressBar.style.width = '0%';
progressText.innerText = "Reading file...";
logContainer.innerHTML = ''; // Clear previous logs
try {
const inputName = 'input' + getExtension(selectedFile.name);
const outputName = 'output.mp4'; // We default to mp4 for simplicity in browser
addLog("Writing file to virtual filesystem...");
// Write file to MEMFS
await ffmpeg.writeFile(inputName, await fetchFile(selectedFile));
// Build Command
const args = ['-i', inputName];
// Video Filters (Scale)
const scaleSetting = scaleRange.value;
if (scaleSetting > 0) {
// Scale to percentage of original
args.push('-vf', `scale=-2:ih*${scaleSetting/100}`);
}
// Codec & Quality
// Using libx265 for better compression (smaller files) or libx264 for compatibility
// If user selects webm, use libvpx-vp9
const format = document.getElementById('formatSelect').value;
const crf = crfRange.value;
if (format === 'webm') {
args.push('-c:v', 'libvpx-vp9', '-crf', crf, '-b:v', '0');
// args.push('-c:v', 'libvpx-vp9', '-crf', crf); // VP9 logic
} else {
args.push('-c:v', 'libx265', '-crf', crf, '-preset', 'fast');
}
// Audio fallback if needed
args.push('-c:a', 'aac', '-b:a', '128k');
args.push(outputName);
addLog("Starting conversion...", "info");
addLog(`Command: ffmpeg ${args.join(' ')}`, "info");
// Execute
await ffmpeg.exec(args);
// Read Output
addLog("Processing complete. Preparing download...");
const data = await ffmpeg.readFile(outputName);
// Create Blob URL
const blob = new Blob([data.buffer], { type: format === 'webm' ? 'video/webm' : 'video/mp4' });
const url = URL.createObjectURL(blob);
// Display Result
outputVideo.src = url;
outputVideo.style.display = 'block';
downloadLink.href = url;
downloadLink.download = `converted_${selectedFile.name.split('.')[0]}.${format}`;
resultArea.style.display = 'block';
progressBar.style.width = '100%';
progressText.innerText = "Done!";
addLog("Success! File ready for download.", "success");
} catch (error) {
addLog(`Error: ${error.message}`, "error");
progressText.innerText = "Error occurred";
convertBtn.disabled = false;
convertBtn.innerHTML = "Start Conversion";
return;
}
// Cleanup
convertBtn.disabled = false;
convertBtn.innerHTML = "Start Conversion";
// Clean up virtual file system to free memory
try {
const inputName = 'input' + getExtension(selectedFile.name);
await ffmpeg.deleteFile(inputName);
await ffmpeg.deleteFile('output.mp4');
} catch(e) {}
});
// Utilities
function addLog(msg, type = 'log') {
const div = document.createElement('div');
div.className = `log-entry log-${type}`;
div.innerText = `[${new Date().toLocaleTimeString()}] ${msg}`;
logContainer.appendChild(div);
logContainer.scrollTop = logContainer.scrollHeight;
}
function updateProgress(progress, time) {
const percentage = Math.round(progress * 100);
progressBar.style.width = `${percentage}%`;
// Format time roughly
const timeStr = time ? ` @ ${Math.round(time)}s` : '';
progressText.innerText = `Processing... ${percentage}%${timeStr}`;
}
function formatBytes(bytes, decimals = 2) {
if (!+bytes) return '0 Bytes';
const k = 1024;
const dm = decimals < 0 ? 0 : decimals;
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(dm))} ${sizes[i]}`;
}
function getExtension(filename) {
return filename.slice((filename.lastIndexOf(".") - 1 >>> 0) + 2);
}
</script>
</body>
</html>