From 8682585ef9dbc1c073a6ef3628b7150c2f71f2d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Ml=C3=A1dek?= Date: Thu, 22 May 2025 09:01:29 +0200 Subject: [PATCH] Initial commit. --- index.html | 61 +++++++++++++++++ syncvid.js | 193 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 254 insertions(+) create mode 100644 index.html create mode 100644 syncvid.js diff --git a/index.html b/index.html new file mode 100644 index 0000000..37fc8e4 --- /dev/null +++ b/index.html @@ -0,0 +1,61 @@ + + + + + + SyncVid + + + +
+ +
+ + + diff --git a/syncvid.js b/syncvid.js new file mode 100644 index 0000000..772576d --- /dev/null +++ b/syncvid.js @@ -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(); +});