293 lines
7.9 KiB
TypeScript
293 lines
7.9 KiB
TypeScript
// issues: initial load fails when syncpoints match, init syncedvideos by checking actual time; unpausing video should also sync; sync max syncpoint
|
|
|
|
type VideoConfig = {
|
|
url: string;
|
|
syncPoint: number;
|
|
};
|
|
|
|
let videos: VideoConfig[] = [];
|
|
let videoElements: HTMLVideoElement[] = [];
|
|
let isMaximizedView = false;
|
|
let maximizedVideoIndex = -1;
|
|
|
|
// Initialize the video grid
|
|
async function initialize(url: string) {
|
|
const response = await fetch(url);
|
|
if (!response.ok) {
|
|
alert(`Failed to fetch videos: ${response.statusText}`);
|
|
return;
|
|
}
|
|
try {
|
|
videos = await response.json();
|
|
} catch (err) {
|
|
alert(`Failed to parse videos: ${err}`);
|
|
return;
|
|
}
|
|
console.log("Fetched data", { videos });
|
|
|
|
const container = document.getElementById("videoContainer");
|
|
if (!container) {
|
|
alert("Video container not found");
|
|
return;
|
|
}
|
|
|
|
// 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 = `${url.split("/").slice(0, -1).join("/")}/${
|
|
videoConfig.url
|
|
}`;
|
|
videoElement.controls = true;
|
|
videoElement.dataset.index = String(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: Event) {
|
|
const target = event.target as HTMLVideoElement;
|
|
console.log("Received play event from", target.dataset.index);
|
|
console.log("Playing all videos");
|
|
const relativeTime =
|
|
target.currentTime - videos[Number(target.dataset.index)].syncPoint;
|
|
for (const videoElement of videoElements) {
|
|
if (
|
|
videoElement.currentTime < videoElement.duration - 1 &&
|
|
relativeTime + videos[Number(videoElement.dataset.index)].syncPoint > 0
|
|
) {
|
|
videoElement.play();
|
|
}
|
|
}
|
|
}
|
|
|
|
function onPause(event: Event) {
|
|
const target = event.target as HTMLVideoElement;
|
|
console.log("Received pause event from", target.dataset.index);
|
|
if (syncing) {
|
|
console.log(
|
|
"Ignoring pause event while syncing from",
|
|
target.dataset.index
|
|
);
|
|
return;
|
|
}
|
|
if (target.currentTime > 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: string[] = [];
|
|
|
|
function onSeek(event: Event) {
|
|
const target = event.target as HTMLVideoElement;
|
|
console.log(
|
|
"Received seeked event from",
|
|
target.dataset.index,
|
|
"at",
|
|
target.currentTime
|
|
);
|
|
|
|
if (syncing) {
|
|
if (syncedVideos.includes(target.dataset.index!)) {
|
|
console.log(" Already synced");
|
|
return;
|
|
}
|
|
syncedVideos.push(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[Number(target.dataset.index)].syncPoint;
|
|
const relativeTime = target.currentTime - syncPoint;
|
|
syncAllVideos(target, relativeTime);
|
|
}
|
|
}
|
|
|
|
function syncAllVideos(
|
|
sourceVideoElement: HTMLVideoElement,
|
|
relativeTime: number
|
|
) {
|
|
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[Number(videoElement.dataset.index)].syncPoint;
|
|
|
|
if (syncedTime < 0) {
|
|
videoElement.currentTime = 0;
|
|
videoElement.pause();
|
|
} else if (
|
|
syncedTime >
|
|
videoElement.duration -
|
|
videos[Number(videoElement.dataset.index)].syncPoint
|
|
) {
|
|
videoElement.currentTime = videoElement.duration;
|
|
} else {
|
|
videoElement.currentTime = syncedTime;
|
|
}
|
|
}
|
|
}
|
|
|
|
function onTimeUpdate(event: Event) {
|
|
const target = event.target as HTMLVideoElement;
|
|
if (syncing || target.paused) {
|
|
return;
|
|
}
|
|
|
|
const relativeTime =
|
|
target.currentTime - videos[Number(target.dataset.index)].syncPoint;
|
|
|
|
for (const videoElement of videoElements) {
|
|
if (
|
|
videoElement.paused &&
|
|
relativeTime + videos[Number(videoElement.dataset.index)].syncPoint > 0
|
|
) {
|
|
console.log(
|
|
"Reached sync point for",
|
|
videoElement.dataset.index,
|
|
"- playing"
|
|
);
|
|
videoElement.play();
|
|
}
|
|
}
|
|
}
|
|
|
|
function toggleMaximizedView(index: number) {
|
|
const container = document.getElementById("videoContainer");
|
|
if (!container) return;
|
|
|
|
if (isMaximizedView && maximizedVideoIndex === index) return;
|
|
|
|
if (index === -1) {
|
|
if (!isMaximizedView) return;
|
|
|
|
container.classList.remove("maximized");
|
|
container.classList.add(`count-${videos.length}`);
|
|
|
|
while (container.firstChild) {
|
|
container.removeChild(container.firstChild);
|
|
}
|
|
|
|
videoElements.forEach((video) => {
|
|
container.appendChild(video);
|
|
});
|
|
|
|
isMaximizedView = false;
|
|
maximizedVideoIndex = -1;
|
|
return;
|
|
}
|
|
|
|
if (index < 0 || index >= videoElements.length) return;
|
|
|
|
container.classList.remove(`count-${videos.length}`);
|
|
container.classList.add("maximized");
|
|
while (container.firstChild) {
|
|
container.removeChild(container.firstChild);
|
|
}
|
|
|
|
const mainVideoDiv = document.createElement("div");
|
|
mainVideoDiv.className = "main-video";
|
|
mainVideoDiv.appendChild(videoElements[index]);
|
|
container.appendChild(mainVideoDiv);
|
|
|
|
const sideVideosDiv = document.createElement("div");
|
|
sideVideosDiv.className = "side-videos";
|
|
container.appendChild(sideVideosDiv);
|
|
videoElements.forEach((video, i) => {
|
|
if (i !== index) {
|
|
const videoWrapper = document.createElement("div");
|
|
videoWrapper.className = "video-wrapper";
|
|
|
|
// Add the video to the wrapper
|
|
videoWrapper.appendChild(video);
|
|
|
|
// Create and add the number label
|
|
const numberLabel = document.createElement("div");
|
|
numberLabel.className = "video-number-label";
|
|
numberLabel.textContent = String(i + 1); // 1-based indexing for display
|
|
videoWrapper.appendChild(numberLabel);
|
|
|
|
// Add the wrapper to the side videos div
|
|
sideVideosDiv.appendChild(videoWrapper);
|
|
}
|
|
});
|
|
|
|
isMaximizedView = true;
|
|
maximizedVideoIndex = index;
|
|
}
|
|
|
|
function handleKeyPress(event: KeyboardEvent) {
|
|
const key = event.key;
|
|
if (/^[0-9]$/.test(key)) {
|
|
const num = parseInt(key, 10);
|
|
if (num === 0) {
|
|
toggleMaximizedView(-1);
|
|
} else if (num <= videoElements.length) {
|
|
toggleMaximizedView(num - 1);
|
|
}
|
|
}
|
|
}
|
|
|
|
document.addEventListener("DOMContentLoaded", async () => {
|
|
try {
|
|
await initialize("content/data.json");
|
|
document.addEventListener("keydown", handleKeyPress);
|
|
} catch (err) {
|
|
console.error("Failed to initialize:", err);
|
|
alert(`Failed to initialize: ${err}`);
|
|
}
|
|
});
|