// 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}`); } });