Initial commit.

This commit is contained in:
Tomáš Mládek 2025-05-22 09:01:29 +02:00
commit 8682585ef9
2 changed files with 254 additions and 0 deletions

61
index.html Normal file
View file

@ -0,0 +1,61 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>SyncVid</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
transition: opacity 0.2s ease-in-out;
}
body {
height: 100vh;
overflow: hidden;
background: black;
}
.video-container {
display: grid;
height: 100vh;
width: 100vw;
}
.syncing {
cursor: wait;
pointer-events: none;
opacity: 0.5;
}
/* Grid layouts based on video count */
.video-container.count-1 {
grid-template-columns: 1fr;
}
.video-container.count-2 {
grid-template-columns: 1fr 1fr;
}
.video-container.count-3 {
grid-template-columns: 1fr 1fr;
grid-template-rows: 1fr 1fr;
}
.video-container.count-3 > video:first-child {
grid-column: span 2;
}
.video-container.count-4 {
grid-template-columns: 1fr 1fr;
grid-template-rows: 1fr 1fr;
}
video {
width: 100%;
height: 100%;
object-fit: contain;
}
</style>
</head>
<body>
<div id="videoContainer" class="video-container">
<!-- Videos will be added here by JavaScript -->
</div>
<script src="syncvid.js"></script>
</body>
</html>

193
syncvid.js Normal file
View file

@ -0,0 +1,193 @@
// issues: initial load fails when syncpoints match, init syncedvideos by checking actual time; unpausing video should also sync; sync max syncpoint
const videos = [
{
url: "vids/test.mp4",
syncPoint: 10,
},
{
url: "vids/test.mp4",
syncPoint: 60 + 10,
},
{
url: "vids/test.mp4",
syncPoint: 120 + 10,
},
];
let videoElements = [];
// Initialize the video grid
function initializeVideos() {
const container = document.getElementById("videoContainer");
// Set the appropriate grid class based on video count
container.classList.add(`count-${videos.length}`);
videos.forEach((videoConfig, index) => {
const videoElement = document.createElement("video");
videoElement.src = videoConfig.url;
videoElement.controls = true;
videoElement.dataset.index = index;
videoElement.preload = "auto";
videoElement.volume = 1 / videos.length;
container.appendChild(videoElement);
videoElements.push(videoElement);
// Add event listeners
videoElement.addEventListener("play", onPlay);
videoElement.addEventListener("pause", onPause);
videoElement.addEventListener("seeked", onSeek);
videoElement.addEventListener("timeupdate", onTimeUpdate);
videoElement.addEventListener("loadeddata", () => {
console.log("Video loaded:", videoElement.dataset.index);
});
});
// Initial sync when videos are loaded
const minSyncVideoIndex = videos.reduce((minIndex, video, index) => {
if (video.syncPoint < videos[minIndex].syncPoint) {
return index;
}
return minIndex;
}, 0);
syncAllVideos(
videoElements[minSyncVideoIndex],
-videos[minSyncVideoIndex].syncPoint
);
syncing = false;
}
function onPlay(event) {
console.log("Received play event from", event.target.dataset.index);
console.log("Playing all videos");
const relativeTime =
event.target.currentTime - videos[event.target.dataset.index].syncPoint;
for (const videoElement of videoElements) {
if (
videoElement.currentTime < videoElement.duration - 1 &&
relativeTime + videos[videoElement.dataset.index].syncPoint > 0
) {
videoElement.play();
}
}
}
function onPause(event) {
console.log("Received pause event from", event.target.dataset.index);
if (syncing) {
console.log(
"Ignoring pause event while syncing from",
event.target.dataset.index
);
return;
}
if (event.target.currentTime > event.target.duration - 1) {
console.log(" Video ended naturally.");
return;
} else {
console.log(" Pausing all videos");
for (const videoElement of videoElements) {
if (videoElement === event.target) {
continue;
}
videoElement.pause();
}
}
}
let syncing = false;
let syncedVideos = [];
function onSeek(event) {
console.log(
"Received seeked event from",
event.target.dataset.index,
"at",
event.target.currentTime
);
if (syncing) {
if (syncedVideos.includes(event.target.dataset.index)) {
console.log(" Already synced");
return;
}
syncedVideos.push(event.target.dataset.index);
if (syncedVideos.length === videoElements.length) {
console.log("Syncing complete");
syncing = false;
document.body.classList.remove("syncing");
}
return;
} else {
console.log(" Received seek while not syncing, starting sync");
const syncPoint = videos[event.target.dataset.index].syncPoint;
const relativeTime = event.target.currentTime - syncPoint;
syncAllVideos(event.target, relativeTime);
}
}
function syncAllVideos(sourceVideoElement, relativeTime) {
syncing = true;
document.body.classList.add("syncing");
syncedVideos = [sourceVideoElement.dataset.index];
console.log(
"Syncing all videos from",
sourceVideoElement.dataset.index,
"at",
sourceVideoElement.currentTime
);
for (const videoElement of videoElements) {
if (videoElement === sourceVideoElement) {
continue;
}
const syncedTime =
relativeTime + videos[videoElement.dataset.index].syncPoint;
if (syncedTime < 0) {
videoElement.currentTime = 0;
videoElement.pause();
} else if (
syncedTime >
videos[videoElement.dataset.index].duration -
videos[videoElement.dataset.index].syncPoint
) {
videoElement.currentTime = videos[videoElement.dataset.index].duration;
} else {
videoElement.currentTime = syncedTime;
}
}
}
function onTimeUpdate(event) {
if (syncing || event.target.paused) {
return;
}
const relativeTime =
event.target.currentTime - videos[event.target.dataset.index].syncPoint;
for (const videoElement of videoElements) {
if (
videoElement.paused &&
relativeTime + videos[videoElement.dataset.index].syncPoint > 0
) {
console.log(
"Reached sync point for",
videoElement.dataset.index,
"- playing"
);
videoElement.play();
}
}
}
// Initialize everything when the page loads
document.addEventListener("DOMContentLoaded", () => {
initializeVideos();
});