Initial commit.
This commit is contained in:
commit
8682585ef9
2 changed files with 254 additions and 0 deletions
61
index.html
Normal file
61
index.html
Normal 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
193
syncvid.js
Normal 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();
|
||||
});
|
Loading…
Add table
Reference in a new issue