<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Video Looper dengan Efek Smooth</title>
<style>
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
line-height: 1.6;
color: #333;
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
padding: 20px;
min-height: 100vh;
}
.container {
max-width: 1000px;
margin: 0 auto;
background: white;
border-radius: 15px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
overflow: hidden;
}
header {
background: linear-gradient(90deg, #3498db, #2c3e50);
color: white;
text-align: center;
padding: 25px 20px;
}
h1 {
font-size: 2.2rem;
margin-bottom: 10px;
}
.subtitle {
font-size: 1.1rem;
opacity: 0.9;
}
.main-content {
padding: 25px;
}
.form-section {
background: #f8f9fa;
padding: 20px;
border-radius: 10px;
margin-bottom: 25px;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
}
.form-group {
margin-bottom: 20px;
}
.full-width {
grid-column: 1 / -1;
}
label {
display: block;
margin-bottom: 8px;
font-weight: 600;
color: #2c3e50;
}
input, select {
width: 100%;
padding: 12px 15px;
border: 2px solid #ddd;
border-radius: 8px;
font-size: 16px;
transition: border-color 0.3s;
}
input:focus, select:focus {
border-color: #3498db;
outline: none;
}
.btn {
padding: 14px 20px;
background: linear-gradient(90deg, #3498db, #2980b9);
color: white;
border: none;
border-radius: 8px;
font-size: 18px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s;
display: inline-flex;
align-items: center;
justify-content: center;
}
.btn:hover {
background: linear-gradient(90deg, #2980b9, #2573a7);
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
}
.btn:disabled {
background: #95a5a6;
cursor: not-allowed;
transform: none;
box-shadow: none;
}
.btn i {
margin-right: 8px;
}
.button-group {
display: flex;
gap: 15px;
margin-top: 20px;
}
.btn-play {
background: linear-gradient(90deg, #2ecc71, #27ae60);
}
.btn-download {
background: linear-gradient(90deg, #e74c3c, #c0392b);
}
.progress-section {
display: none;
margin: 25px 0;
}
.progress-container {
height: 20px;
background: #ecf0f1;
border-radius: 10px;
overflow: hidden;
margin-bottom: 10px;
}
.progress-bar {
height: 100%;
background: linear-gradient(90deg, #2ecc71, #27ae60);
width: 0%;
transition: width 0.3s;
}
.status {
text-align: center;
font-weight: 500;
color: #2c3e50;
}
.video-section {
display: none;
text-align: center;
margin: 25px 0;
}
.video-container {
background: #000;
border-radius: 10px;
overflow: hidden;
margin: 20px 0;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
position: relative;
max-width: 100%;
margin-left: auto;
margin-right: auto;
}
video {
max-width: 100%;
display: block;
}
.download-section {
display: none;
margin: 25px 0;
}
.download-options {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px;
margin-top: 20px;
}
.download-option {
background: white;
border-radius: 10px;
padding: 20px;
text-align: center;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1);
transition: transform 0.3s;
}
.download-option:hover {
transform: translateY(-5px);
}
.download-option h3 {
color: #2c3e50;
margin-bottom: 15px;
}
.info-box {
background: #e3f2fd;
border-left: 4px solid #2196f3;
padding: 15px;
border-radius: 4px;
margin: 20px 0;
text-align: left;
}
.info-title {
font-weight: 600;
margin-bottom: 5px;
color: #2196f3;
}
.looping-info {
display: flex;
justify-content: space-around;
margin: 20px 0;
flex-wrap: wrap;
}
.info-card {
background: white;
border-radius: 8px;
padding: 15px;
margin: 10px;
flex: 1;
min-width: 200px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
text-align: center;
}
.info-card h3 {
color: #2c3e50;
margin-bottom: 10px;
}
.info-card p {
font-size: 1.5rem;
font-weight: bold;
color: #3498db;
}
.effect-options {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 15px;
margin-top: 15px;
}
.effect-option {
padding: 12px;
border: 2px solid #ddd;
border-radius: 8px;
text-align: center;
cursor: pointer;
transition: all 0.3s;
}
.effect-option:hover {
border-color: #3498db;
}
.effect-option.selected {
border-color: #3498db;
background-color: #e3f2fd;
}
footer {
text-align: center;
padding: 20px;
color: #7f8c8d;
font-size: 0.9rem;
border-top: 1px solid #eee;
}
@media (max-width: 768px) {
.form-section {
grid-template-columns: 1fr;
}
.container {
border-radius: 10px;
}
h1 {
font-size: 1.8rem;
}
.main-content {
padding: 15px;
}
.looping-info {
flex-direction: column;
}
.button-group {
flex-direction: column;
}
}
</style>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
</head>
<body>
<div class="container">
<header>
<h1>Video Looper dengan Efek Smooth</h1>
<p class="subtitle">Gabungkan video dengan efek transisi dan download hasilnya</p>
</header>
<div class="main-content">
<div class="form-section">
<div class="form-group full-width">
<label for="videoFile">Pilih Video (MP4):</label>
<input type="file" id="videoFile" accept="video/mp4">
</div>
<div class="form-group">
<label for="targetDuration">Durasi Looping (menit):</label>
<input type="number" id="targetDuration" min="1" value="60">
</div>
<div class="form-group">
<label for="transitionEffect">Efek Transisi:</label>
<select id="transitionEffect">
<option value="fade">Fade</option>
<option value="crossfade" selected>Crossfade</option>
<option value="blur">Blur</option>
<option value="slide">Slide</option>
<option value="none">Tidak Ada</option>
</select>
</div>
<div class="form-group full-width">
<label>Efek Tambahan:</label>
<div class="effect-options">
<div class="effect-option selected" data-effect="shadow">
<i class="fas fa-border-all"></i>
<p>Shadow</p>
</div>
<div class="effect-option" data-effect="glow">
<i class="fas fa-sun"></i>
<p>Glow</p>
</div>
<div class="effect-option" data-effect="vignette">
<i class="fas fa-circle"></i>
<p>Vignette</p>
</div>
</div>
</div>
</div>
<div class="button-group">
<button id="previewBtn" class="btn btn-play" disabled>
<i class="fas fa-play"></i> Preview Looping
</button>
<button id="processBtn" class="btn">
<i class="fas fa-cog"></i> Proses & Download
</button>
</div>
<div class="progress-section" id="progressSection">
<div class="progress-container">
<div class="progress-bar" id="progressBar"></div>
</div>
<div class="status" id="statusText">Mempersiapkan proses looping...</div>
</div>
<div class="looping-info" id="loopingInfo">
<div class="info-card">
<h3>Durasi Asli Video</h3>
<p id="originalDurationText">00:00</p>
</div>
<div class="info-card">
<h3>Jumlah Loop</h3>
<p id="loopCount">0</p>
</div>
<div class="info-card">
<h3>Total Durasi</h3>
<p id="totalDuration">00:00</p>
</div>
</div>
<div class="video-section" id="videoSection">
<h2>Preview Video Looping</h2>
<div class="video-container">
<video id="outputVideo" controls></video>
</div>
</div>
<div class="download-section" id="downloadSection">
<h2>Download Video Looping</h2>
<p>Pilih kualitas untuk download:</p>
<div class="download-options">
<div class="download-option">
<h3>Kualitas Rendah</h3>
<p>(Lebih Cepat)</p>
<button class="btn btn-download download-res" data-quality="low">
<i class="fas fa-download"></i> Download
</button>
</div>
<div class="download-option">
<h3>Kualitas Sedang</h3>
<p>(Rekomendasi)</p>
<button class="btn btn-download download-res" data-quality="medium">
<i class="fas fa-download"></i> Download
</button>
</div>
<div class="download-option">
<h3>Kualitas Tinggi</h3>
<p>(Lebih Lama)</p>
<button class="btn btn-download download-res" data-quality="high">
<i class="fas fa-download"></i> Download
</button>
</div>
</div>
</div>
<div class="info-box">
<div class="info-title">Cara Kerja Video Looper:</div>
<p>• Video akan digabungkan dengan efek transisi smooth di antara loop</p>
<p>• Layout video asli akan dipertahankan tanpa perubahan aspect ratio</p>
<p>• Proses lebih cepat dengan teknik pemrosesan yang dioptimasi</p>
<p>• Hasil akhir dapat didownload dalam berbagai kualitas</p>
</div>
</div>
<footer>
<p>Video Looper dengan Efek Smooth © 2023</p>
</footer>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
const videoFileInput = document.getElementById('videoFile');
const targetDurationInput = document.getElementById('targetDuration');
const transitionEffectSelect = document.getElementById('transitionEffect');
const previewBtn = document.getElementById('previewBtn');
const processBtn = document.getElementById('processBtn');
const progressSection = document.getElementById('progressSection');
const progressBar = document.getElementById('progressBar');
const statusText = document.getElementById('statusText');
const videoSection = document.getElementById('videoSection');
const outputVideo = document.getElementById('outputVideo');
const downloadSection = document.getElementById('downloadSection');
const originalDurationText = document.getElementById('originalDurationText');
const loopCountElem = document.getElementById('loopCount');
const totalDurationElem = document.getElementById('totalDuration');
const loopingInfo = document.getElementById('loopingInfo');
const effectOptions = document.querySelectorAll('.effect-option');
const downloadButtons = document.querySelectorAll('.download-res');
let originalVideo = null;
let originalVideoDuration = 0;
let originalVideoWidth = 0;
let originalVideoHeight = 0;
let processedVideoBlob = null;
let selectedEffect = 'shadow';
// Pilih efek
effectOptions.forEach(option => {
option.addEventListener('click', function() {
effectOptions.forEach(opt => opt.classList.remove('selected'));
this.classList.add('selected');
selectedEffect = this.dataset.effect;
});
});
// Format waktu dari detik ke menit:detik
function formatTime(seconds) {
const mins = Math.floor(seconds / 60);
const secs = Math.floor(seconds % 60);
return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
}
// Validasi input
function validateInputs() {
const isVideoSelected = videoFileInput.files.length > 0;
previewBtn.disabled = !isVideoSelected;
}
videoFileInput.addEventListener('change', function() {
validateInputs();
if (videoFileInput.files.length > 0) {
const file = videoFileInput.files[0];
const videoUrl = URL.createObjectURL(file);
// Buat video element untuk membaca metadata
const video = document.createElement('video');
video.src = videoUrl;
video.onloadedmetadata = function() {
originalVideoDuration = video.duration;
originalVideoWidth = video.videoWidth;
originalVideoHeight = video.videoHeight;
originalDurationText.textContent = formatTime(originalVideoDuration);
loopingInfo.style.display = 'flex';
// Hitung looping
calculateLooping();
};
}
});
targetDurationInput.addEventListener('input', calculateLooping);
function calculateLooping() {
if (originalVideoDuration > 0) {
const targetDuration = parseInt(targetDurationInput.value) * 60; // konversi ke detik
const loopCount = Math.ceil(targetDuration / originalVideoDuration);
const totalDuration = loopCount * originalVideoDuration;
loopCountElem.textContent = loopCount;
totalDurationElem.textContent = formatTime(totalDuration);
}
}
// Preview video looping
previewBtn.addEventListener('click', function() {
if (videoFileInput.files.length === 0) return;
const file = videoFileInput.files[0];
const videoUrl = URL.createObjectURL(file);
outputVideo.src = videoUrl;
videoSection.style.display = 'block';
// Atur event listener untuk looping
outputVideo.onended = function() {
// Tambahkan efek transisi
const transitionEffect = transitionEffectSelect.value;
applyTransitionEffect(outputVideo, transitionEffect, function() {
outputVideo.currentTime = 0;
outputVideo.play();
});
};
outputVideo.play();
});
// Fungsi untuk menerapkan efek transisi
function applyTransitionEffect(videoElement, effect, callback) {
switch(effect) {
case 'fade':
videoElement.style.transition = 'opacity 0.5s ease-in-out';
videoElement.style.opacity = '0';
setTimeout(() => {
callback();
setTimeout(() => {
videoElement.style.opacity = '1';
}, 50);
}, 500);
break;
case 'crossfade':
videoElement.style.transition = 'opacity 0.8s ease-in-out';
videoElement.style.opacity = '0.5';
setTimeout(() => {
callback();
setTimeout(() => {
videoElement.style.opacity = '1';
}, 50);
}, 800);
break;
case 'blur':
videoElement.style.transition = 'filter 0.6s ease-in-out';
videoElement.style.filter = 'blur(5px)';
setTimeout(() => {
callback();
setTimeout(() => {
videoElement.style.filter = 'blur(0)';
}, 50);
}, 600);
break;
default:
callback();
}
}
// Proses video untuk download
processBtn.addEventListener('click', async function() {
const targetDurationMinutes = parseInt(targetDurationInput.value);
const targetDuration = targetDurationMinutes * 60; // Konversi ke detik
const transitionEffect = transitionEffectSelect.value;
// Validasi
if (!originalVideoDuration || originalVideoDuration <= 0) {
statusText.textContent = 'Error: Durasi video asli tidak valid';
statusText.style.color = '#e74c3c';
return;
}
// Hitung berapa kali looping diperlukan
const loopCount = Math.ceil(targetDuration / originalVideoDuration);
// Tampilkan progress
progressSection.style.display = 'block';
statusText.textContent = 'Mempersiapkan proses looping...';
statusText.style.color = '#2c3e50';
try {
// Load video yang dipilih
const file = videoFileInput.files[0];
const videoUrl = URL.createObjectURL(file);
// Buat video element untuk membaca video asli
originalVideo = document.createElement('video');
originalVideo.src = videoUrl;
originalVideo.muted = true;
// Tunggu sampai metadata video dimuat
await new Promise((resolve, reject) => {
originalVideo.onloadedmetadata = resolve;
originalVideo.onerror = reject;
// Timeout untuk error handling
setTimeout(() => {
reject(new Error('Timeout saat memuat video'));
}, 10000);
});
// Buat canvas untuk rendering dengan ukuran yang sama dengan video asli
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
// Pertahankan ukuran asli video
canvas.width = originalVideoWidth;
canvas.height = originalVideoHeight;
// Buat media recorder untuk merekam output
const stream = canvas.captureStream(30); // 30 FPS
const recorder = new MediaRecorder(stream, {
mimeType: 'video/mp4; codecs="avc1.42E01E"',
videoBitsPerSecond: 3000000 // Bitrate tetap untuk kualitas baik
});
const chunks = [];
recorder.ondataavailable = e => chunks.push(e.data);
recorder.onstop = () => {
processedVideoBlob = new Blob(chunks, { type: 'video/mp4' });
// Tampilkan video hasil
const processedVideoUrl = URL.createObjectURL(processedVideoBlob);
outputVideo.src = processedVideoUrl;
videoSection.style.display = 'block';
downloadSection.style.display = 'block';
statusText.textContent = 'Proses looping selesai! Kini Anda dapat mendownload video.';
statusText.style.color = '#27ae60';
};
// Mulai merekam
recorder.start();
// Render frame per frame
let currentLoop = 0;
let currentTime = 0;
let lastFrameTime = 0;
statusText.textContent = `Memproses looping: 0/${loopCount}`;
function renderFrame(timestamp) {
if (currentLoop >= loopCount) {
recorder.stop();
return;
}
// Batasi frame rate untuk performa lebih baik
if (!lastFrameTime || timestamp - lastFrameTime >= 1000/30) {
lastFrameTime = timestamp;
// Atur waktu video asli berdasarkan posisi dalam loop
const videoTime = currentTime % originalVideoDuration;
// Pastikan videoTime adalah nilai finite yang valid
if (!isFinite(videoTime) || videoTime < 0) {
statusText.textContent = 'Error: Waktu video tidak valid';
statusText.style.color = '#e74c3c';
recorder.stop();
return;
}
// Atur currentTime video
originalVideo.currentTime = videoTime;
// Tunggu sampai video siap
originalVideo.onseeked = function() {
// Render frame ke canvas
ctx.clearRect(0, 0, canvas.width, canvas.height);
// Terapkan efek shadow jika dipilih
if (selectedEffect === 'shadow') {
ctx.shadowColor = 'rgba(0, 0, 0, 0.4)';
ctx.shadowBlur = 10;
ctx.shadowOffsetX = 0;
ctx.shadowOffsetY = 0;
} else if (selectedEffect === 'glow') {
ctx.shadowColor = 'rgba(255, 255, 255, 0.5)';
ctx.shadowBlur = 15;
ctx.shadowOffsetX = 0;
ctx.shadowOffsetY = 0;
} else if (selectedEffect === 'vignette') {
// Efek vignette akan ditambahkan setelah drawImage
}
// Gambar video ke canvas
ctx.drawImage(originalVideo, 0, 0, canvas.width, canvas.height);
// Terapkan efek vignette jika dipilih
if (selectedEffect === 'vignette') {
const gradient = ctx.createRadialGradient(
canvas.width / 2, canvas.height / 2, 0,
canvas.width / 2, canvas.height / 2, Math.max(canvas.width, canvas.height) / 2
);
gradient.addColorStop(0, 'rgba(0,0,0,0)');
gradient.addColorStop(0.7, 'rgba(0,0,0,0)');
gradient.addColorStop(1, 'rgba(0,0,0,0.5)');
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, canvas.width, canvas.height);
}
// Reset shadow
ctx.shadowColor = 'transparent';
ctx.shadowBlur = 0;
// Update progress
currentTime += 1/30; // 1 frame pada 30 FPS
// Cek jika sudah mencapai akhir video dalam loop ini
if (videoTime + 1/30 >= originalVideoDuration) {
currentLoop++;
const progress = (currentLoop / loopCount) * 100;
progressBar.style.width = `${progress}%`;
statusText.textContent = `Memproses looping: ${currentLoop}/${loopCount}`;
}
};
originalVideo.onerror = function() {
statusText.textContent = 'Error: Gagal memproses frame video';
statusText.style.color = '#e74c3c';
recorder.stop();
};
}
// Request frame berikutnya
requestAnimationFrame(renderFrame);
}
// Pastikan video diputar untuk menghindari issue pada beberapa browser
originalVideo.play().then(() => {
// Jeda video setelah mulai diputar
originalVideo.pause();
// Mulai proses rendering
requestAnimationFrame(renderFrame);
}).catch(error => {
console.error('Error memutar video:', error);
statusText.textContent = 'Error: Tidak dapat memproses video';
statusText.style.color = '#e74c3c';
});
} catch (error) {
console.error('Error processing video:', error);
statusText.textContent = 'Error: ' + error.message;
statusText.style.color = '#e74c3c';
}
});
// Download handlers
downloadButtons.forEach(button => {
button.addEventListener('click', function() {
if (!processedVideoBlob) return;
const quality = this.dataset.quality;
let fileName = 'video-looping.mp4';
// Sesuaikan kualitas berdasarkan pilihan
if (quality === 'low') {
fileName = 'video-looping-cepat.mp4';
} else if (quality === 'high') {
fileName = 'video-looping-hd.mp4';
}
const a = document.createElement('a');
a.href = URL.createObjectURL(processedVideoBlob);
a.download = fileName;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
});
});
});
</script>
</body>
</html>