anycoder-a9e31f6f / index.html
aavi21458's picture
Upload folder using huggingface_hub
f42cd9d verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Audio-Video Lip Sync Studio</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
:root {
--primary: #6366f1;
--primary-dark: #4f46e5;
--primary-light: #818cf8;
--secondary: #ec4899;
--secondary-dark: #db2777;
--bg-dark: #0f0f1a;
--bg-card: #1a1a2e;
--bg-card-hover: #252542;
--text-primary: #ffffff;
--text-secondary: #a1a1aa;
--text-muted: #71717a;
--border: #27272a;
--success: #10b981;
--warning: #f59e0b;
--error: #ef4444;
--gradient-1: linear-gradient(135deg, #6366f1, #ec4899);
--gradient-2: linear-gradient(135deg, #8b5cf6, #06b6d4);
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Inter', sans-serif;
background: var(--bg-dark);
color: var(--text-primary);
min-height: 100vh;
overflow-x: hidden;
}
/* Animated Background */
.bg-animation {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
z-index: 0;
overflow: hidden;
}
.bg-animation::before {
content: '';
position: absolute;
top: -50%;
left: -50%;
width: 200%;
height: 200%;
background:
radial-gradient(ellipse at 20% 20%, rgba(99, 102, 241, 0.15) 0%, transparent 50%),
radial-gradient(ellipse at 80% 80%, rgba(236, 72, 153, 0.1) 0%, transparent 50%);
animation: bgPulse 15s ease-in-out infinite;
}
@keyframes bgPulse {
0%, 100% { transform: translate(0, 0) rotate(0deg); }
33% { transform: translate(2%, 2%) rotate(1deg); }
66% { transform: translate(-1%, 1%) rotate(-1deg); }
}
/* Header */
header {
position: relative;
z-index: 10;
padding: 1.5rem 2rem;
background: rgba(15, 15, 26, 0.8);
backdrop-filter: blur(20px);
border-bottom: 1px solid var(--border);
display: flex;
justify-content: space-between;
align-items: center;
}
.logo {
display: flex;
align-items: center;
gap: 0.75rem;
}
.logo-icon {
width: 42px;
height: 42px;
background: var(--gradient-1);
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.25rem;
box-shadow: 0 4px 20px rgba(99, 102, 241, 0.4);
}
.logo h1 {
font-size: 1.25rem;
font-weight: 700;
background: var(--gradient-1);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.brand-link {
color: var(--text-secondary);
text-decoration: none;
font-size: 0.75rem;
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 1rem;
background: var(--bg-card);
border-radius: 20px;
transition: all 0.3s ease;
}
.brand-link:hover {
background: var(--bg-card-hover);
color: var(--primary-light);
}
/* Main Content */
main {
position: relative;
z-index: 1;
padding: 2rem;
max-width: 1600px;
margin: 0 auto;
}
/* Upload Section */
.upload-section {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
gap: 1.5rem;
margin-bottom: 2rem;
}
.upload-card {
background: var(--bg-card);
border-radius: 20px;
padding: 2rem;
border: 2px dashed var(--border);
transition: all 0.3s ease;
position: relative;
overflow: hidden;
}
.upload-card::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 4px;
background: var(--gradient-1);
opacity: 0;
transition: opacity 0.3s ease;
}
.upload-card:hover {
border-color: var(--primary);
transform: translateY(-4px);
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3);
}
.upload-card:hover::before {
opacity: 1;
}
.upload-card.video::before {
background: var(--gradient-2);
}
.upload-card.has-file {
border-style: solid;
border-color: var(--success);
}
.upload-icon {
width: 80px;
height: 80px;
background: rgba(99, 102, 241, 0.1);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto 1.5rem;
font-size: 2rem;
color: var(--primary);
transition: all 0.3s ease;
}
.video .upload-icon {
background: rgba(139, 92, 246, 0.1);
color: #8b5cf6;
}
.upload-card:hover .upload-icon {
transform: scale(1.1) rotate(5deg);
}
.upload-title {
font-size: 1.25rem;
font-weight: 600;
margin-bottom: 0.5rem;
text-align: center;
}
.upload-subtitle {
color: var(--text-secondary);
font-size: 0.875rem;
text-align: center;
margin-bottom: 1.5rem;
}
.file-input {
display: none;
}
.upload-btn {
width: 100%;
padding: 1rem;
background: var(--gradient-1);
border: none;
border-radius: 12px;
color: white;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
}
.video .upload-btn {
background: var(--gradient-2);
}
.upload-btn:hover {
transform: translateY(-2px);
box-shadow: 0 10px 30px rgba(99, 102, 241, 0.4);
}
.file-info {
display: none;
margin-top: 1rem;
padding: 1rem;
background: rgba(16, 185, 129, 0.1);
border-radius: 12px;
text-align: center;
}
.file-info.visible {
display: block;
}
.file-info i {
color: var(--success);
margin-right: 0.5rem;
}
.file-name {
font-weight: 600;
color: var(--success);
word-break: break-all;
}
/* Sync Controls */
.sync-section {
background: var(--bg-card);
border-radius: 24px;
padding: 2rem;
margin-bottom: 2rem;
border: 1px solid var(--border);
}
.section-header {
display: flex;
align-items: center;
gap: 1rem;
margin-bottom: 2rem;
}
.section-icon {
width: 48px;
height: 48px;
background: var(--gradient-1);
border-radius: 14px;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.25rem;
}
.section-title {
font-size: 1.5rem;
font-weight: 700;
}
.sync-controls {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 1.5rem;
}
.control-group {
background: rgba(255, 255, 255, 0.03);
padding: 1.5rem;
border-radius: 16px;
border: 1px solid var(--border);
}
.control-label {
display: flex;
align-items: center;
gap: 0.75rem;
font-weight: 600;
margin-bottom: 1rem;
color: var(--text-secondary);
}
.control-label i {
color: var(--primary);
}
.slider-container {
position: relative;
}
.time-slider {
width: 100%;
height: 8px;
-webkit-appearance: none;
background: var(--border);
border-radius: 4px;
outline: none;
cursor: pointer;
}
.time-slider::-webkit-slider-thumb {
-webkit-appearance: none;
width: 24px;
height: 24px;
background: var(--gradient-1);
border-radius: 50%;
cursor: pointer;
box-shadow: 0 4px 15px rgba(99, 102, 241, 0.5);
transition: transform 0.2s ease;
}
.time-slider::-webkit-slider-thumb:hover {
transform: scale(1.2);
}
.time-value {
text-align: center;
font-size: 1.5rem;
font-weight: 700;
color: var(--primary);
margin-top: 1rem;
font-variant-numeric: tabular-nums;
}
.btn-group {
display: flex;
gap: 0.75rem;
margin-top: 1rem;
}
.btn {
flex: 1;
padding: 0.875rem 1.5rem;
border: none;
border-radius: 12px;
font-size: 0.875rem;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
}
.btn-primary {
background: var(--primary);
color: white;
}
.btn-primary:hover {
background: var(--primary-dark);
transform: translateY(-2px);
}
.btn-secondary {
background: var(--bg-card-hover);
color: var(--text-primary);
border: 1px solid var(--border);
}
.btn-secondary:hover {
background: var(--border);
}
/* Preview Section */
.preview-section {
display: grid;
grid-template-columns: 2fr 1fr;
gap: 2rem;
margin-bottom: 2rem;
}
@media (max-width: 1024px) {
.preview-section {
grid-template-columns: 1fr;
}
}
.video-container {
background: var(--bg-card);
border-radius: 24px;
padding: 1.5rem;
border: 1px solid var(--border);
}
.video-wrapper {
position: relative;
width: 100%;
background: #000;
border-radius: 16px;
overflow: hidden;
aspect-ratio: 16/9;
}
.video-wrapper video {
width: 100%;
height: 100%;
object-fit: contain;
}
.video-placeholder {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #1a1a2e 0%, #0f0f1a 100%);
color: var(--text-muted);
}
.video-placeholder i {
font-size: 4rem;
margin-bottom: 1rem;
opacity: 0.5;
}
/* Waveform */
.waveform-container {
background: var(--bg-card);
border-radius: 24px;
padding: 1.5rem;
border: 1px solid var(--border);
}
.waveform-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
}
.waveform-title {
font-weight: 600;
display: flex;
align-items: center;
gap: 0.5rem;
}
.waveform-title i {
color: var(--primary);
}
#waveform-canvas {
width: 100%;
height: 150px;
background: rgba(0, 0, 0, 0.3);
border-radius: 12px;
}
/* Playback Controls */
.playback-controls {
background: var(--bg-card);
border-radius: 24px;
padding: 2rem;
border: 1px solid var(--border);
margin-bottom: 2rem;
}
.main-controls {
display: flex;
align-items: center;
justify-content: center;
gap: 1rem;
margin-bottom: 1.5rem;
}
.control-btn {
width: 56px;
height: 56px;
border-radius: 50%;
border: none;
background: var(--bg-card-hover);
color: var(--text-primary);
font-size: 1.25rem;
cursor: pointer;
transition: all 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
}
.control-btn:hover {
background: var(--primary);
transform: scale(1.1);
}
.control-btn.play {
width: 72px;
height: 72px;
background: var(--gradient-1);
font-size: 1.5rem;
box-shadow: 0 8px 30px rgba(99, 102, 241, 0.4);
}
.control-btn.play:hover {
transform: scale(1.15);
}
.progress-container {
position: relative;
margin-bottom: 1rem;
}
.progress-bar {
width: 100%;
height: 10px;
background: var(--border);
border-radius: 5px;
cursor: pointer;
position: relative;
overflow: hidden;
}
.progress-fill {
height: 100%;
background: var(--gradient-1);
border-radius: 5px;
width: 0%;
transition: width 0.1s linear;
}
.time-display {
display: flex;
justify-content: space-between;
font-size: 0.875rem;
color: var(--text-secondary);
font-variant-numeric: tabular-nums;
}
.volume-control {
display: flex;
align-items: center;
gap: 1rem;
justify-content: center;
}
.volume-slider {
width: 120px;
height: 6px;
-webkit-appearance: none;
background: var(--border);
border-radius: 3px;
cursor: pointer;
}
.volume-slider::-webkit-slider-thumb {
-webkit-appearance: none;
width: 16px;
height: 16px;
background: var(--primary);
border-radius: 50%;
cursor: pointer;
}
/* Sync Status */
.sync-status {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1rem;
margin-top: 1.5rem;
}
.status-card {
background: rgba(255, 255, 255, 0.03);
padding: 1.25rem;
border-radius: 12px;
border: 1px solid var(--border);
text-align: center;
}
.status-value {
font-size: 1.5rem;
font-weight: 700;
color: var(--primary);
margin-bottom: 0.25rem;
}
.status-label {
font-size: 0.75rem;
color: var(--text-secondary);
text-transform: uppercase;
letter-spacing: 0.5px;
}
.status-card.synced .status-value {
color: var(--success);
}
.status-card.warning .status-value {
color: var(--warning);
}
/* Export Section */
.export-section {
background: var(--bg-card);
border-radius: 24px;
padding: 2rem;
border: 1px solid var(--border);
text-align: center;
}
.export-info {
margin-bottom: 1.5rem;
color: var(--text-secondary);
}
.export-actions {
display: flex;
gap: 1rem;
justify-content: center;
flex-wrap: wrap;
}
.btn-export {
padding: 1rem 2rem;
border-radius: 12px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
display: flex;
align-items: center;
gap: 0.75rem;
border: none;
}
.btn-export.sync {
background: var(--gradient-1);
color: white;
}
.btn-export.sync:hover {
transform: translateY(-3px);
box-shadow: 0 15px 40px rgba(99, 102, 241, 0.4);
}
.btn-export.reset {
background: transparent;
color: var(--text-secondary);
border: 2px solid var(--border);
}
.btn-export.reset:hover {
border-color: var(--error);
color: var(--error);
}
/* Toast Notifications */
.toast-container {
position: fixed;
top: 2rem;
right: 2rem;
z-index: 1000;
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.toast {
padding: 1rem 1.5rem;
background: var(--bg-card);
border-radius: 12px;
border-left: 4px solid var(--primary);
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.4);
animation: slideIn 0.3s ease;
display: flex;
align-items: center;
gap: 0.75rem;
}
.toast.success {
border-color: var(--success);
}
.toast.error {
border-color: var(--error);
}
.toast.warning {
border-color: var(--warning);
}
@keyframes slideIn {
from {
transform: translateX(100%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
/* Responsive */
@media (max-width: 768px) {
header {
padding: 1rem;
flex-direction: column;
gap: 1rem;
}
main {
padding: 1rem;
}
.upload-section {
grid-template-columns: 1fr;
}
.sync-section,
.playback-controls {
padding: 1.5rem;
}
.control-btn {
width: 48px;
height: 48px;
}
.control-btn.play {
width: 64px;
height: 64px;
}
}
/* Loading Animation */
.loading {
display: inline-block;
width: 20px;
height: 20px;
border: 2px solid var(--border);
border-top-color: var(--primary);
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
/* Visual Feedback for Sync */
.sync-indicator {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 1rem;
background: rgba(16, 185, 129, 0.1);
border-radius: 20px;
font-size: 0.875rem;
color: var(--success);
}
.sync-indicator.unsynced {
background: rgba(239, 68, 68, 0.1);
color: var(--error);
}
.sync-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: currentColor;
animation: pulse 2s ease-in-out infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
</style>
</head>
<body>
<div class="bg-animation"></div>
<header>
<div class="logo">
<div class="logo-icon">
<i class="fas fa-wave-square"></i>
</div>
<h1>Lip Sync Studio</h1>
</div>
<a href="https://huggingface.co/spaces/akhaliq/anycoder" target="_blank" class="brand-link">
<i class="fas fa-robot"></i>
Built with anycoder
</a>
</header>
<main>
<!-- Upload Section -->
<section class="upload-section">
<div class="upload-card video" id="video-upload-card">
<div class="upload-icon">
<i class="fas fa-video"></i>
</div>
<h3 class="upload-title">Upload Video</h3>
<p class="upload-subtitle">Select an MP4 file to use as the video source</p>
<input type="file" id="video-input" class="file-input" accept="video/mp4,video/webm">
<button class="upload-btn" onclick="document.getElementById('video-input').click()">
<i class="fas fa-folder-open"></i>
Choose Video File
</button>
<div class="file-info" id="video-file-info">
<i class="fas fa-check-circle"></i>
<span class="file-name" id="video-filename"></span>
</div>
</div>
<div class="upload-card" id="audio-upload-card">
<div class="upload-icon">
<i class="fas fa-music"></i>
</div>
<h3 class="upload-title">Upload Audio</h3>
<p class="upload-subtitle">Select an MP3 file to synchronize</p>
<input type="file" id="audio-input" class="file-input" accept="audio/mp3,audio/wav,audio/mpeg">
<button class="upload-btn" onclick="document.getElementById('audio-input').click()">
<i class="fas fa-folder-open"></i>
Choose Audio File
</button>
<div class="file-info" id="audio-file-info">
<i class="fas fa-check-circle"></i>
<span class="file-name" id="audio-filename"></span>
</div>
</div>
</section>
<!-- Preview Section -->
<section class="preview-section">
<div class="video-container">
<div class="waveform-header">
<span class="waveform-title">
<i class="fas fa-desktop"></i>
Video Preview
</span>
<span class="sync-indicator unsynced" id="sync-indicator">
<span class="sync-dot"></span>
<span id="sync-text">Not Synced</span>
</span>
</div>
<div class="video-wrapper">
<video id="video-player" playsinline></video>
<div class="video-placeholder" id="video-placeholder">
<i class="fas fa-film"></i>
<span>Video will appear here</span>
</div>
</div>
</div>
<div class="waveform-container">
<div class="waveform-header">
<span class="waveform-title">
<i class="fas fa-wave-square"></i>
Audio Waveform
</span>
</div>
<canvas id="waveform-canvas"></canvas>
</div>
</section>
<!-- Sync Controls -->
<section class="sync-section">
<div class="section-header">
<div class="section-icon">
<i class="fas fa-sliders-h"></i>
</div>
<h2 class="section-title">Synchronization Controls</h2>
</div>
<div class="sync-controls">
<div class="control-group">
<div class="control-label">
<i class="fas fa-clock"></i>
Audio Offset (Delay)
</div>
<div class="slider-container">
<input type="range" class="time-slider" id="offset-slider" min="-3000" max="3000" value="0" step="10">
</div>
<div class="time-value" id="offset-value">0ms</div>
<div class="btn-group">
<button class="btn btn-secondary" onclick="adjustOffset(-100)">
<i class="fas fa-minus"></i> -100ms
</button>
<button class="btn btn-secondary" onclick="adjustOffset(100)">
<i class="fas fa-plus"></i> +100ms
</button>
</div>
</div>
<div class="control-group">
<div class="control-label">
<i class="fas fa-play"></i>
Playback Rate
</div>
<div class="slider-container">
<input type="range" class="time-slider" id="rate-slider" min="0.5" max="2" value="1" step="0.05">
</div >
<div class="time-value" id="rate-value">1.0x</div>
<div class="btn-group">
<button class="btn btn-secondary" onclick="adjustRate(-0.1)">
<i class="fas fa-minus"></i> Slower
</button>
<button class="btn btn-secondary" onclick="adjustRate(0.1)">
<i class="fas fa-plus"></i> Faster
</button>
</div>
</div>
<div class="control-group">
<div class="control-label">
<i class="fas fa-volume-up"></i>
Audio Volume
</div>
<div class="slider-container">
<input type="range" class="time-slider" id="volume-slider" min="0" max="2" value="1" step="0.1">
</div>
<div class="time-value" id="volume-value">100%</div>
<div class="btn-group">
<button class="btn btn-secondary" onclick="setVolume(0)">
<i class="fas fa-volume-mute"></i> Mute
</button>
<button class="btn btn-secondary" onclick="setVolume(1)">
<i class="fas fa-volume-up"></i> Normal
</button>
</div>
</div>
</div>
</section>
<!-- Playback Controls -->
<section class="playback-controls">
<div class="main-controls">
<button class="control-btn" id="skip-back-btn" title="Skip Back 5s">
<i class="fas fa-backward"></i>
</button>
<button class="control-btn" id="rewind-btn" title="Rewind 1s">
<i class="fas fa-step-backward"></i>
</button>
<button class="control-btn play" id="play-btn" title="Play/Pause">
<i class="fas fa-play"></i>
</button>
<button class="control-btn" id="forward-btn" title="Forward 1s">
<i class="fas fa-step-forward"></i>
</button>
<button class="control-btn" id="skip-forward-btn" title="Skip Forward 5s">
<i class="fas fa-forward"></i>
</button>
</div>
<div class="progress-container">
<div class="progress-bar" id="progress-bar">
<div class="progress-fill" id="progress-fill"></div>
</div>
<div class="time-display">
<span id="current-time">00:00.000</span>
<span id="duration">00:00.000</span>
</div>
</div>
<div class="volume-control">
<i class="fas fa-volume-down" style="color: var(--text-secondary);"></i>
<input type="range" class="volume-slider" id="main-volume" min="0" max="1" value="1" step="0.01">
<i class="fas fa-volume-up" style="color: var(--text-secondary);"></i>
</div>
<div class="sync-status">
<div class="status-card" id="offset-status">
<div class="status-value" id="status-offset">0ms</div>
<div class="status-label">Audio Offset</div>
</div>
<div class="status-card" id="rate-status">
<div class="status-value" id="status-rate">1.0x</div>
<div class="status-label">Playback Rate</div>
</div>
<div class="status-card" id="video-status">
<div class="status-value" id="status-video">--</div>
<div class="status-label">Video Loaded</div>
</div>
<div class="status-card" id="audio-status">
<div class="status-value" id="status-audio">--</div>
<div class="status-label">Audio Loaded</div>
</div>
</div>
</section>
<!-- Export Section -->
<section class="export-section">
<div class="section-header" style="justify-content: center;">
<div class="section-icon">
<i class="fas fa-download"></i>
</div>
<h2 class="section-title">Export Settings</h2>
</div>
<p class="export-info">Save your synchronization settings to apply them later or share with others.</p>
<div class="export-actions">
<button class="btn-export sync" onclick="exportSettings()">
<i class="fas fa-save"></i>
Save Sync Settings
</button>
<button class="btn-export reset" onclick="resetAll()">
<i class="fas fa-undo"></i>
Reset All
</button>
</div>
</section>
</main>
<!-- Toast Container -->
<div class="toast-container" id="toast-container"></div>
<script>
// Global state
const state = {
videoFile: null,
audioFile: null,
audioContext: null,
audioBuffer: null,
audioSource: null,
isPlaying: false,
audioOffset: 0,
playbackRate: 1,
volume: 1,
startTime: 0,
pausedAt: 0,
videoElement: null,
audioElement: null
};
// DOM Elements
const elements = {
videoInput: document.getElementById('video-input'),
audioInput: document.getElementById('audio-input'),
videoPlayer: document.getElementById('video-player'),
videoPlaceholder: document.getElementById('video-placeholder'),
waveformCanvas: document.getElementById('waveform-canvas'),
playBtn: document.getElementById('play-btn'),
progressBar: document.getElementById('progress-bar'),
progressFill: document.getElementById('progress-fill'),
currentTime: document.getElementById('current-time'),
duration: document.getElementById('duration'),
offsetSlider: document.getElementById('offset-slider'),
offsetValue: document.getElementById('offset-value'),
rateSlider: document.getElementById('rate-slider'),
rateValue: document.getElementById('rate-value'),
volumeSlider: document.getElementById('volume-slider'),
volumeValue: document.getElementById('volume-value'),
mainVolume: document.getElementById('main-volume'),
syncIndicator: document.getElementById('sync-indicator'),
syncText: document.getElementById('sync-text')
};
// Initialize
document.addEventListener('DOMContentLoaded', () => {
initializeEventListeners();
drawEmptyWaveform();
});
function initializeEventListeners() {
// File inputs
elements.videoInput.addEventListener('change', handleVideoUpload);
elements.audioInput.addEventListener('change', handleAudioUpload);
// Playback controls
elements.playBtn.addEventListener('click', togglePlay);
document.getElementById('skip-back-btn').addEventListener('click', () => skip(-5));
document.getElementById('rewind-btn').addEventListener('click', () => skip(-1));
document.getElementById('forward-btn').addEventListener('click', () => skip(1));
document.getElementById('skip-forward-btn').addEventListener('click', () => skip(5));
// Progress bar
elements.progressBar.addEventListener('click', seek);
// Sliders
elements.offsetSlider.addEventListener('input', updateOffset);
elements.rateSlider.addEventListener('input', updateRate);
elements.volumeSlider.addEventListener('input', updateVolume);
elements.mainVolume.addEventListener('input', (e) => {
const vol = parseFloat(e.target.value);
setVolume(vol);
});
// Video events
elements.videoPlayer.addEventListener('loadedmetadata', updateDuration);
elements.videoPlayer.addEventListener('timeupdate', updateProgress);
elements.videoPlayer.addEventListener('ended', () => {
state.isPlaying = false;
updatePlayButton();
});
}
// File Upload Handlers
function handleVideoUpload(e) {
const file = e.target.files[0];
if (!file) return;
state.videoFile = file;
const url = URL.createObjectURL(file);
elements.videoPlayer.src = url;
elements.videoPlayer.load();
elements.videoPlaceholder.style.display = 'none';
// Update UI
document.getElementById('video-upload-card').classList.add('has-file');
document.getElementById('video-filename').textContent = file.name;
document.getElementById('video-file-info').classList.add('visible');
document.getElementById('status-video').textContent = '✓ Loaded';
showToast('Video loaded successfully!', 'success');
checkSyncStatus();
}
async function handleAudioUpload(e) {
const file = e.target.files[0];
if (!file) return;
state.audioFile = file;
try {
// Create audio element for playback
if (state.audioElement) {
state.audioElement.pause();
state.audioElement = null;
}
const audioUrl = URL.createObjectURL(file);
state.audioElement = new Audio(audioUrl);
// Setup Web Audio API for visualization
await setupAudioContext(file);
// Update UI
document.getElementById('audio-upload-card').classList.add('has-file');
document.getElementById('audio-filename').textContent = file.name;
document.getElementById('audio-file-info').classList.add('visible');
document.getElementById('status-audio').textContent = '✓ Loaded';
showToast('Audio loaded successfully!', 'success');
checkSyncStatus();
} catch (error) {
showToast('Error loading audio: ' + error.message, 'error');
}
}
async function setupAudioContext(file) {
if (!state.audioContext) {
state.audioContext = new (window.AudioContext || window.webkitAudioContext)();
}
const arrayBuffer = await file.arrayBuffer();
state.audioBuffer = await state.audioContext.decodeAudioData(arrayBuffer);
drawWaveform();
}
// Waveform Drawing
function drawEmptyWaveform() {
const canvas = elements.waveformCanvas;
const ctx = canvas.getContext('2d');
const dpr = window.devicePixelRatio || 1;
canvas.width = canvas.offsetWidth * dpr;
canvas.height = canvas.offsetHeight * dpr;
ctx.scale(dpr, dpr);
const width = canvas.offsetWidth;
const height = canvas.offsetHeight;
// Background
ctx.fillStyle = 'rgba(0, 0, 0, 0.3)';
ctx.fillRect(0, 0, width, height);
// Center line
ctx.strokeStyle = 'rgba(99, 102, 241, 0.3)';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(0, height / 2);
ctx.lineTo(width, height / 2);
ctx.stroke();
// Text
ctx.fillStyle = 'rgba(161, 161, 170, 0.5)';
ctx.font = '14px Inter';
ctx.textAlign = 'center';
ctx.fillText('Upload audio to see waveform', width / 2, height / 2 + 5);
}
function drawWaveform() {
if (!state.audioBuffer) return;
const canvas = elements.waveformCanvas;
const ctx = canvas.getContext('2d');
const dpr = window.devicePixelRatio || 1;
canvas.width = canvas.offsetWidth * dpr;
canvas.height = canvas.offsetHeight * dpr;
ctx.scale(dpr, dpr);
const width = canvas.offsetWidth;
const height = canvas.offsetHeight;
const data = state.audioBuffer.getChannelData(0);
const step = Math.ceil(data.length / width);
const amp = height / 2;
// Clear
ctx.fillStyle = 'rgba(0, 0, 0, 0.3)';
ctx.fillRect(0, 0, width, height);
// Draw waveform
const gradient = ctx.createLinearGradient(0, 0, width, 0);
gradient.addColorStop(0, '#6366f1');
gradient.addColorStop(0.5, '#8b5cf6');
gradient.addColorStop(1, '#ec4899');
ctx.fillStyle = gradient;
ctx.beginPath();
ctx.moveTo(0, amp);
for (let i = 0; i < width; i++) {
let min = 1.0;
let max = -1.0;
for (let j = 0; j < step; j++) {
const datum = data[(i * step) + j];
if (datum < min) min = datum;
if (datum > max) max = datum;
}
ctx.lineTo(i, (1 + min) * amp);
ctx.lineTo(i, (1 + max) * amp);
}
ctx.closePath();
ctx.fill();
// Center line
ctx.strokeStyle = 'rgba(255, 255, 255, 0.1)';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(0, height / 2);
ctx.lineTo(width, height / 2);
ctx.stroke();
}
// Playback Controls
function togglePlay() {
if (!state.videoFile && !state.audioFile) {
showToast('Please upload a video or audio file first', 'warning');
return;
}
if (state.isPlaying) {
pause();
} else {
play();
}
}
function play() {
state.isPlaying = true;
if (state.videoFile) {
elements.videoPlayer.play();
}
if (state.audioFile && state.audioElement) {
if (state.audioContext && state.audioContext.state === 'suspended') {
state.audioContext.resume();
}
state.audioElement.currentTime = elements.videoPlayer.currentTime + (state.audioOffset / 1000);
state.audioElement.playbackRate = state.playbackRate;
state.audioElement.volume = state.volume;
state.audioElement.play();
}
updatePlayButton();
showToast('Playback started', 'success');
}
function pause() {
state.isPlaying = false;
if (state.videoFile) {
elements.videoPlayer.pause();
}
if (state.audioElement) {
state.audioElement.pause();
}
updatePlayButton();
}
function updatePlayButton() {
const icon = elements.playBtn.querySelector('i');
icon.className = state.isPlaying ? 'fas fa-pause' : 'fas fa-play';
}
function skip(seconds) {
if (state.videoFile) {
elements.videoPlayer.currentTime = Math.max(0, elements.videoPlayer.currentTime + seconds);
}
}
function seek(e) {
const rect = elements.progressBar.getBoundingClientRect();
const pos = (e.clientX - rect.left) / rect.width;
const duration = elements.videoPlayer.duration || 0;
if (state.videoFile) {
elements.videoPlayer.currentTime = pos * duration;
}
if (state.audioElement) {
state.audioElement.currentTime = pos * duration;
}
}
function updateProgress() {
if (!state.videoFile) return;
const current = elements.videoPlayer.currentTime;
const duration = elements.videoPlayer.duration || 1;
const percent = (current / duration) * 100;
elements.progressFill.style.width = percent + '%';
elements.currentTime.textContent = formatTime(current);
// Sync audio position
if (state.audioElement && state.isPlaying) {
const audioTime = current + (state.audioOffset / 1000);
if (Math.abs(state.audioElement.currentTime - audioTime) > 0.1) {
state.audioElement.currentTime = audioTime;
}
}
}
function updateDuration() {
const duration = elements.videoPlayer.duration;
elements.duration.textContent = formatTime(duration);
}
function formatTime(seconds) {
if (isNaN(seconds)) return '00:00.000';
const mins = Math.floor(seconds / 60);
const secs = Math.floor(seconds % 60);
const ms = Math.floor((seconds % 1) * 1000);
return `${mins.toString().padStart(2,