Skip to content

Instantly share code, notes, and snippets.

@enosh
Last active November 24, 2024 18:33
Show Gist options
  • Select an option

  • Save enosh/620fee2e6893b651cf136839a445e648 to your computer and use it in GitHub Desktop.

Select an option

Save enosh/620fee2e6893b651cf136839a445e648 to your computer and use it in GitHub Desktop.
Synced video playback with drag-and-drop
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Synced Video Playback</title>
<style>
body {
font-family: SF Pro, Arial, sans-serif;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
box-sizing: border-box;
}
.video-container {
display: flex;
position: relative;
box-sizing: border-box;
margin: 0 1em;
}
video {
height: 100%;
min-width: 10%;
}
#presenterVideo, #screenShareVideo {
width: 50%;
}
.resizer {
width: 1em;
cursor: ew-resize;
top: 0;
bottom: 0;
z-index: 10;
}
.resizerBar {
width: 4px;
height: 100%;
margin: auto;
background-color: lightgray;
transition: background-color 0.2s ease;
position: relative;
}
.resizer:hover .resizerBar {
opacity: 1;
background-color: Highlight;
}
.drop-zone {
border: 2px dashed #ccc;
padding: 10px;
text-align: center;
margin-bottom: 10px;
cursor: pointer;
width: 90%;
max-width: 1000px;
}
.controls {
margin-top: 1em;
display: flex;
justify-content: center;
gap: 0.5em;
}
</style>
</head>
<body>
<h1>Synced Video Playback with Drag-and-Drop</h1>
<div class="drop-zone" id="dropZone">
Drag and drop two videos here (Presenter and Screen Share).
</div>
<div class="video-container">
<video id="presenterVideo" controls></video>
<div class="resizer" id="resizer">
<div class="resizerBar"></div>
</div>
<video id="screenShareVideo" controls muted></video>
</div>
<div class="controls">
<button id="playPause">Play</button>
<button id="mute">Mute</button>
<label for="speed">Speed:</label>
<select id="speed">
<option value="0.5">0.5x</option>
<option value="1" selected>1x</option>
<option value="1.5">1.5x</option>
<option value="1.75">1.75x</option>
<option value="2">2x</option>
</select>
<button id="skipBack">-10s</button>
<button id="skipForward">+10s</button>
<button id="swapVideos">Swap Videos</button>
</div>
<script>
const presenterVideo = document.getElementById('presenterVideo');
const screenShareVideo = document.getElementById('screenShareVideo');
const dropZone = document.getElementById('dropZone');
const resizer = document.getElementById('resizer');
const playPauseBtn = document.getElementById('playPause');
const muteBtn = document.getElementById('mute');
const speedSelect = document.getElementById('speed');
let isResizing = false;
let syncInterval = null;
let positionSaveInterval = null;
// Save and load position helpers
const savePosition = (key, position) => {
localStorage.setItem(key, position);
};
const loadPosition = (key) => {
const position = parseFloat(localStorage.getItem(key));
return isNaN(position) ? 0 : position;
};
// Handle drag-and-drop for videos
dropZone.addEventListener('dragover', (event) => {
event.preventDefault();
dropZone.style.borderColor = '#000';
});
dropZone.addEventListener('dragleave', () => {
dropZone.style.borderColor = '#ccc';
});
dropZone.addEventListener('drop', (event) => {
event.preventDefault();
dropZone.style.borderColor = '#ccc';
const videoFiles = Array.from(event.dataTransfer.files).filter(file => file.type.startsWith('video/'));
if (videoFiles.length === 2) {
const presenterFile = videoFiles[0];
const screenShareFile = videoFiles[1];
const presenterKey = `video-position-${presenterFile.name}`;
const screenShareKey = `video-position-${screenShareFile.name}`;
presenterVideo.src = URL.createObjectURL(presenterFile);
screenShareVideo.src = URL.createObjectURL(screenShareFile);
presenterVideo.addEventListener('loadedmetadata', () => {
const position = loadPosition(presenterKey);
presenterVideo.currentTime = position < presenterVideo.duration ? position : 0;
});
screenShareVideo.addEventListener('loadedmetadata', () => {
const position = loadPosition(screenShareKey);
screenShareVideo.currentTime = position < screenShareVideo.duration ? position : 0;
});
presenterVideo.addEventListener('pause', () => savePosition(presenterKey, presenterVideo.currentTime));
presenterVideo.addEventListener('ended', () => savePosition(presenterKey, 0));
screenShareVideo.addEventListener('pause', () => savePosition(screenShareKey, screenShareVideo.currentTime));
screenShareVideo.addEventListener('ended', () => savePosition(screenShareKey, 0));
dropZone.textContent = 'Videos loaded. Ready to play!';
// Save positions every 20 seconds
if (positionSaveInterval) clearInterval(positionSaveInterval);
positionSaveInterval = setInterval(() => {
savePosition(presenterKey, presenterVideo.currentTime);
savePosition(screenShareKey, screenShareVideo.currentTime);
}, 20000);
} else {
alert('Please drop exactly two video files.');
}
});
// Play/Pause both videos
playPauseBtn.addEventListener('click', () => {
if (presenterVideo.paused && screenShareVideo.paused) {
presenterVideo.play();
screenShareVideo.play();
playPauseBtn.textContent = 'Pause';
// Set playback speed when clicking play
const speed = parseFloat(speedSelect.value);
presenterVideo.playbackRate = speed;
screenShareVideo.playbackRate = speed;
startSyncing();
} else {
presenterVideo.pause();
screenShareVideo.pause();
playPauseBtn.textContent = 'Play';
stopSyncing();
}
});
// Sync play/pause between both videos
let isPageVisible = true;
// Track visibility change
document.addEventListener('visibilitychange', () => {
isPageVisible = !document.hidden;
});
const syncPlayPause = (sourceVideo, targetVideo) => {
['play', 'pause'].forEach(eventType => {
sourceVideo.addEventListener(eventType, () => {
// Allows playback to continue when tab is backgrounded
if (!isPageVisible) return;
if (eventType === 'play' && targetVideo.paused) {
targetVideo.play();
} else if (eventType === 'pause' && !targetVideo.paused) {
targetVideo.pause();
}
});
});
};
syncPlayPause(presenterVideo, screenShareVideo);
syncPlayPause(screenShareVideo, presenterVideo);
// Mute/Unmute presenter audio (screen share is muted by default)
muteBtn.addEventListener('click', () => {
const isMuted = presenterVideo.muted;
presenterVideo.muted = !isMuted;
muteBtn.textContent = isMuted ? 'Mute' : 'Unmute';
});
// Playback speed
const savePlaybackSpeed = (key, speed) => {
localStorage.setItem(key, speed);
};
const loadPlaybackSpeed = (key) => {
const speed = parseFloat(localStorage.getItem(key));
return isNaN(speed) ? 1 : speed; // Default to 1x if no speed is stored
};
// Load playback speed on initialization
const playbackSpeedKey = 'video-playback-speed';
const initialSpeed = loadPlaybackSpeed(playbackSpeedKey);
presenterVideo.playbackRate = initialSpeed;
screenShareVideo.playbackRate = initialSpeed;
speedSelect.value = initialSpeed.toString();
// Change playback speed and save it
speedSelect.addEventListener('change', () => {
const speed = parseFloat(speedSelect.value);
presenterVideo.playbackRate = speed;
screenShareVideo.playbackRate = speed;
savePlaybackSpeed(playbackSpeedKey, speed);
});
// Skip buttons
const skipBackBtn = document.getElementById('skipBack');
const skipForwardBtn = document.getElementById('skipForward');
skipBackBtn.addEventListener('click', () => {
presenterVideo.currentTime = Math.max(0, presenterVideo.currentTime - 10);
screenShareVideo.currentTime = Math.max(0, screenShareVideo.currentTime - 10);
});
skipForwardBtn.addEventListener('click', () => {
presenterVideo.currentTime = Math.min(presenterVideo.duration, presenterVideo.currentTime + 10);
screenShareVideo.currentTime = Math.min(screenShareVideo.duration, screenShareVideo.currentTime + 10);
});
const swapVideosBtn = document.getElementById('swapVideos');
// Swap the videos between presenter and screen share
swapVideosBtn.addEventListener('click', () => {
// Store the current src attributes temporarily
const presenterSrc = presenterVideo.src;
const screenShareSrc = screenShareVideo.src;
// Swap the sources
presenterVideo.src = screenShareSrc;
screenShareVideo.src = presenterSrc;
// Swap the current playback positions
const presenterTime = presenterVideo.currentTime;
presenterVideo.currentTime = screenShareVideo.currentTime;
screenShareVideo.currentTime = presenterTime;
// Ensure that mute status remains correct
screenShareVideo.muted = true;
presenterVideo.muted = false;
});
// Sync videos periodically
const startSyncing = () => {
if (syncInterval) return;
syncInterval = setInterval(() => {
const presenterTime = presenterVideo.currentTime;
const screenShareTime = screenShareVideo.currentTime;
const drift = Math.abs(presenterTime - screenShareTime);
if (drift > 0.1) {
screenShareVideo.currentTime = presenterTime;
}
}, 100);
};
const stopSyncing = () => {
clearInterval(syncInterval);
syncInterval = null;
};
// Resizer logic
resizer.addEventListener('mousedown', (event) => {
isResizing = true;
document.body.style.cursor = 'ew-resize';
});
const getMinWidth = (element) => {
const computedStyle = window.getComputedStyle(element);
return parseFloat(computedStyle.minWidth); // Get min-width as a float, already in pixels or percentage
};
// Resizing logic with min-width and snapping
const snapThreshold = 10;
document.addEventListener('mousemove', (event) => {
if (!isResizing) return;
// Get the width of the resizer from the CSS
const resizerWidth = parseFloat(getComputedStyle(resizer).width);
const containerWidth = document.querySelector('.video-container').clientWidth;
const offsetLeft = document.querySelector('.video-container').offsetLeft;
// Calculate the new width for the presenter video while respecting resizer width
let newPresenterWidth = event.clientX - offsetLeft;
// Check if the resizer is near the center and snap if within the threshold
const centerPosition = containerWidth / 2;
if (Math.abs(newPresenterWidth - centerPosition) < snapThreshold) {
newPresenterWidth = centerPosition;
}
// Ensure the presenter video width doesn't exceed container bounds or become smaller than minimum width
newPresenterWidth = Math.max(newPresenterWidth, getMinWidth(presenterVideo));
newPresenterWidth = Math.min(newPresenterWidth, containerWidth - getMinWidth(screenShareVideo) - (resizerWidth / 2));
// Calculate the new width for the screen share video
const newScreenShareWidth = containerWidth - newPresenterWidth;
// Update the widths while maintaining size constraints
presenterVideo.style.flexBasis = `${newPresenterWidth}px`;
screenShareVideo.style.flexBasis = `${newScreenShareWidth}px`;
// Adjust the position of the resizer
resizer.style.left = `${newPresenterWidth}px`;
});
document.addEventListener('mouseup', () => {
if (isResizing) {
isResizing = false;
document.body.style.cursor = 'default';
}
});
const arrowSkipSec = 10;
const jlSkipSec = 5;
// Keyboard shortcuts
document.addEventListener('keydown', (event) => {
switch (event.key) {
case ' ': // Play/Pause
event.preventDefault();
playPauseBtn.click();
break;
case 'm': // Mute/Unmute
muteBtn.click();
break;
case 'ArrowRight': // Skip forward 5 seconds
presenterVideo.currentTime = Math.min(presenterVideo.duration, presenterVideo.currentTime + jlSkipSec);
screenShareVideo.currentTime = Math.min(screenShareVideo.duration, screenShareVideo.currentTime + jlSkipSec);
break;
case 'ArrowLeft': // Skip backward 5 seconds
presenterVideo.currentTime = Math.max(0, presenterVideo.currentTime - jlSkipSec);
screenShareVideo.currentTime = Math.max(0, screenShareVideo.currentTime - jlSkipSec);
break;
case 'l': // Skip forward 10 seconds
presenterVideo.currentTime = Math.min(presenterVideo.duration, presenterVideo.currentTime + arrowSkipSec);
screenShareVideo.currentTime = Math.min(screenShareVideo.duration, screenShareVideo.currentTime + arrowSkipSec);
break;
case 'j': // Skip backward 10 seconds
presenterVideo.currentTime = Math.max(0, presenterVideo.currentTime - arrowSkipSec);
screenShareVideo.currentTime = Math.max(0, screenShareVideo.currentTime - arrowSkipSec);
break;
case '>': // Increase playback speed
speedSelect.selectedIndex = Math.min(speedSelect.options.length - 1, speedSelect.selectedIndex + 1);
speedSelect.dispatchEvent(new Event('change'));
break;
case '<': // Decrease playback speed
speedSelect.selectedIndex = Math.max(0, speedSelect.selectedIndex - 1);
speedSelect.dispatchEvent(new Event('change'));
break;
}
});
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment