594 lines
		
	
	
		
			21 KiB
		
	
	
	
		
			HTML
		
	
	
	
	
	
			
		
		
	
	
			594 lines
		
	
	
		
			21 KiB
		
	
	
	
		
			HTML
		
	
	
	
	
	
<!DOCTYPE html>
 | 
						|
<html lang="en">
 | 
						|
<head>
 | 
						|
	<meta charset="UTF-8">
 | 
						|
	<title>Viva++</title>
 | 
						|
	<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.7.1/jquery.min.js"></script>
 | 
						|
	<style>
 | 
						|
		body {
 | 
						|
			background-color: #000;
 | 
						|
			background-image: radial-gradient(circle at 50% 200%, rgba(255, 0, 0, 0.4) 0%, transparent 70%),
 | 
						|
				radial-gradient(circle at 50% -100%, rgba(0, 0, 255, 0.4) 0%, transparent 70%);
 | 
						|
			margin: 0;
 | 
						|
			padding: 0;
 | 
						|
			display: flex;
 | 
						|
			justify-content: center;
 | 
						|
			align-items: center;
 | 
						|
			height: 100vh;
 | 
						|
			overflow: hidden; /* To prevent any unexpected scrollbars */
 | 
						|
		}
 | 
						|
 | 
						|
		.loader {
 | 
						|
			position: fixed; /* Changed for global overlay */
 | 
						|
			left: 50%;
 | 
						|
			top: 50%;
 | 
						|
			transform: translate(-50%, -50%); /* Center on screen */
 | 
						|
			z-index: 10001; /* Above video overlay */
 | 
						|
			border: 1.5vw solid #333;
 | 
						|
			border-top: 1.5vw solid #3498db;
 | 
						|
			border-bottom: 1.5vw solid #3498db;
 | 
						|
			border-radius: 50%;
 | 
						|
			width: 10vw;
 | 
						|
			height: 10vw;
 | 
						|
			animation: spin 2s linear infinite;
 | 
						|
		}
 | 
						|
 | 
						|
		@keyframes spin {
 | 
						|
			0% { transform: rotate(0deg); }
 | 
						|
			100% { transform: rotate(360deg); }
 | 
						|
		}
 | 
						|
 | 
						|
		#video-container {
 | 
						|
			display: none;
 | 
						|
			width: 100%;
 | 
						|
			justify-content: center;
 | 
						|
			align-items: center;
 | 
						|
			gap: 2vw;
 | 
						|
		}
 | 
						|
 | 
						|
		.video-thumbnail {
 | 
						|
			border-radius: 8px;
 | 
						|
			box-shadow: 0 4px 8px rgba(0, 0, 0, 0.5);
 | 
						|
			background-color: #444;
 | 
						|
			aspect-ratio: 16/9;
 | 
						|
			width: 20vw;
 | 
						|
			transition: width 0.3s ease, box-shadow 0.3s ease, left 0.3s ease;
 | 
						|
			transform: translate(-50%, 0);
 | 
						|
			position: fixed;
 | 
						|
		}
 | 
						|
 | 
						|
		.video-thumbnail.focussed {
 | 
						|
			outline: 3px solid #557; /* Existing focus outline */
 | 
						|
			box-shadow: 0 0 400px -10px #446; /* Existing focus shadow */
 | 
						|
			width: 50vw;
 | 
						|
			z-index: 3; /* Ensure focused thumbnail is on top */
 | 
						|
		}
 | 
						|
 | 
						|
		.side-video {
 | 
						|
			width: 20vw;
 | 
						|
			opacity: 0.7;
 | 
						|
			z-index: 1;
 | 
						|
		}
 | 
						|
 | 
						|
		.play-button {
 | 
						|
			position: absolute;
 | 
						|
			top: 50%;
 | 
						|
			left: 50%;
 | 
						|
			transform: translate(-50%, -50%);
 | 
						|
			width: 80px; /* Adjust size as needed */
 | 
						|
			height: 80px; /* Adjust size as needed */
 | 
						|
			background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='white'%3E%3Cpath d='M8 5v14l11-7z'/%3E%3C/svg%3E");
 | 
						|
			background-color: rgba(0, 0, 0, 0.5); /* Semi-transparent background */
 | 
						|
			background-repeat: no-repeat;
 | 
						|
			background-position: center;
 | 
						|
			background-size: 50%; /* Adjust icon size within button */
 | 
						|
			border-radius: 50%; /* Circular button */
 | 
						|
			cursor: pointer;
 | 
						|
			z-index: 3; /* Ensure it's above the video thumbnails */
 | 
						|
			border: 2px solid white;
 | 
						|
			box-shadow: 0 0 15px rgba(0,0,0,0.7);
 | 
						|
		}
 | 
						|
 | 
						|
		#video-overlay-container {
 | 
						|
			position: fixed;
 | 
						|
			top: 0;
 | 
						|
			left: 0;
 | 
						|
			width: 100%;
 | 
						|
			height: 100%;
 | 
						|
			background-color: black;
 | 
						|
			z-index: 10000; /* Above other page content */
 | 
						|
			display: none; /* Initially hidden, JS will change to flex */
 | 
						|
			align-items: center;
 | 
						|
			justify-content: center;
 | 
						|
		}
 | 
						|
 | 
						|
		#video-overlay-container .video-player {
 | 
						|
			width: 100%;
 | 
						|
			height: 100%;
 | 
						|
			display: block;
 | 
						|
			position: fixed;
 | 
						|
		}
 | 
						|
 | 
						|
		#video-overlay-container #back-button { /* Back button is now inside the container */
 | 
						|
			position: absolute;
 | 
						|
			top: 20px;
 | 
						|
			left: 20px;
 | 
						|
			z-index: 1; /* z-index relative to siblings in container */
 | 
						|
			padding: 10px 20px;
 | 
						|
			font-size: 16px;
 | 
						|
			background-color: rgba(0, 0, 0, 0.5);
 | 
						|
			color: white;
 | 
						|
			border: 1px solid white;
 | 
						|
			border-radius: 5px;
 | 
						|
			cursor: pointer;
 | 
						|
			opacity: 1; /* Visible by default */
 | 
						|
			transition: opacity 0.3s ease-in-out; /* Smooth transition */
 | 
						|
		}
 | 
						|
 | 
						|
		#video-overlay-container #back-button.button-hidden {
 | 
						|
			opacity: 0; /* Class to hide the button */
 | 
						|
		}
 | 
						|
 | 
						|
		#custom-video-controls {
 | 
						|
			position: absolute;
 | 
						|
			bottom: 0;
 | 
						|
			left: 0;
 | 
						|
			width: 100%;
 | 
						|
			padding: 10px;
 | 
						|
			background-color: rgba(0, 0, 0, 0.5);
 | 
						|
			display: flex;
 | 
						|
			align-items: center;
 | 
						|
			z-index: 1; /* Above video, but below back button if it were in the same direct stacking context */
 | 
						|
			opacity: 1;
 | 
						|
			transition: opacity 0.3s ease-in-out;
 | 
						|
			box-sizing: border-box;
 | 
						|
		}
 | 
						|
 | 
						|
		#custom-video-controls.controls-hidden {
 | 
						|
			opacity: 0;
 | 
						|
		}
 | 
						|
 | 
						|
		#custom-play-pause-button {
 | 
						|
			background: none;
 | 
						|
			border: none;
 | 
						|
			color: white;
 | 
						|
			font-size: 1.5em; /* Adjust size as needed */
 | 
						|
			cursor: pointer;
 | 
						|
			padding: 5px 10px;
 | 
						|
			margin-right: 10px;
 | 
						|
		}
 | 
						|
 | 
						|
		#custom-seek-bar {
 | 
						|
			flex-grow: 1;
 | 
						|
			cursor: pointer;
 | 
						|
			accent-color: #3498db; /* Optional: color for the seek bar thumb and progress */
 | 
						|
		}
 | 
						|
 | 
						|
		/* Styles for TV remote focus indication */
 | 
						|
		#back-button:focus {
 | 
						|
			outline: 3px solid #3498db !important; /* A clear blue outline */
 | 
						|
			box-shadow: 0 0 15px #3498db; /* A glow effect */
 | 
						|
		}
 | 
						|
 | 
						|
		.nofocus:focus {
 | 
						|
			outline: none !important; /* Remove focus outline for elements with nofocus class */
 | 
						|
		}
 | 
						|
	</style>
 | 
						|
	<script>
 | 
						|
		function hideSpinner() { // Was hideLoader, now only hides spinner
 | 
						|
            $(".loader").hide();
 | 
						|
		}
 | 
						|
 | 
						|
        function showSpinner() { // Was showLoader
 | 
						|
			$(".loader").show();
 | 
						|
		}
 | 
						|
 | 
						|
        function showVideoContainer() {
 | 
						|
            $("#video-container").css("display", "flex");
 | 
						|
        }
 | 
						|
 | 
						|
        let currentVideo = null;
 | 
						|
		let videoElement = null;
 | 
						|
		let overlayContainer = null;
 | 
						|
		let inactivityTimer = null;
 | 
						|
		let backButton = null;
 | 
						|
		let customControls = null;
 | 
						|
		let playPauseButton = null;
 | 
						|
		let seekBar = null;
 | 
						|
 | 
						|
		function exitVideoPlayback() {
 | 
						|
			if (videoElement && videoElement.pause) {
 | 
						|
				videoElement.pause();
 | 
						|
			}
 | 
						|
			clearTimeout(inactivityTimer);
 | 
						|
			if (overlayContainer) {
 | 
						|
				overlayContainer.css('cursor', 'default');
 | 
						|
				overlayContainer.off('mousemove.inactivityControls'); // Remove session-specific listener
 | 
						|
				overlayContainer.hide(); // Hide instead of remove
 | 
						|
			}
 | 
						|
 | 
						|
			$(document).off('fullscreenchange.videoPlayback webkitfullscreenchange.videoPlayback mozFullScreen.videoPlayback MSFullscreenChange.videoPlayback', handleFullscreenChange);
 | 
						|
 | 
						|
			if (videoElement) {
 | 
						|
				$(videoElement).off('.currentVideo'); // Remove all namespaced video events
 | 
						|
				videoElement.src = ""; // Clear the source
 | 
						|
				videoElement.load(); // Important to ensure the video unloads
 | 
						|
			}
 | 
						|
			if (seekBar) { // seekBar is a jQuery object
 | 
						|
				seekBar.val(0);
 | 
						|
				seekBar.css('background', ''); // Reset seek bar style
 | 
						|
			}
 | 
						|
			updatePlayPauseButtonState(); // Update button to show play icon usually
 | 
						|
 | 
						|
			// Attempt to exit fullscreen
 | 
						|
			if (document.fullscreenElement || document.webkitIsFullScreen || document.mozFullScreen || document.msFullscreenElement) {
 | 
						|
				if (document.exitFullscreen) {
 | 
						|
					document.exitFullscreen().catch(() => {});
 | 
						|
				} else if (document.mozCancelFullScreen) {
 | 
						|
					document.mozCancelFullScreen();
 | 
						|
				} else if (document.webkitExitFullscreen) {
 | 
						|
					document.webkitExitFullscreen();
 | 
						|
				} else if (document.msExitFullscreen) {
 | 
						|
					document.msExitFullscreen();
 | 
						|
				}
 | 
						|
			}
 | 
						|
			// FOCUS: Return focus to the main view's play button
 | 
						|
			if ($("#video-container").is(":visible") && $('#play-button').length) {
 | 
						|
				$('#play-button').focus();
 | 
						|
			}
 | 
						|
		}
 | 
						|
 | 
						|
		function showControlsAndResetTimer() {
 | 
						|
			if (backButton) backButton.removeClass('button-hidden');
 | 
						|
			if (customControls) customControls.removeClass('controls-hidden');
 | 
						|
			if (overlayContainer) overlayContainer.css('cursor', 'default');
 | 
						|
 | 
						|
			clearTimeout(inactivityTimer);
 | 
						|
			inactivityTimer = setTimeout(function() {
 | 
						|
				if (backButton) backButton.addClass('button-hidden');
 | 
						|
				if (customControls) customControls.addClass('controls-hidden');
 | 
						|
				if (overlayContainer) overlayContainer.css('cursor', 'none');
 | 
						|
			}, 3000);
 | 
						|
		}
 | 
						|
 | 
						|
		function handlePlayPauseClick() {
 | 
						|
			if (videoElement) {
 | 
						|
				if (videoElement.paused) {
 | 
						|
					videoElement.play();
 | 
						|
				} else {
 | 
						|
					videoElement.pause();
 | 
						|
				}
 | 
						|
			}
 | 
						|
            updatePlayPauseButtonState();
 | 
						|
		}
 | 
						|
 | 
						|
		function updatePlayPauseButtonState() {
 | 
						|
			if (!playPauseButton || !videoElement) return;
 | 
						|
			if (videoElement.paused) {
 | 
						|
				playPauseButton.text('►');
 | 
						|
			} else {
 | 
						|
				playPauseButton.text('❚❚');
 | 
						|
			}
 | 
						|
		}
 | 
						|
 | 
						|
		function handleVideoLoadedMetadata() {
 | 
						|
			if (seekBar && videoElement) {
 | 
						|
				seekBar.attr('max', videoElement.duration);
 | 
						|
			}
 | 
						|
		}
 | 
						|
 | 
						|
		function handleVideoTimeUpdate() {
 | 
						|
			if (seekBar && videoElement && videoElement.duration) { // Check videoElement.duration to prevent NaN
 | 
						|
				seekBar.val(videoElement.currentTime);
 | 
						|
				const percentage = (videoElement.currentTime / videoElement.duration) * 100;
 | 
						|
				seekBar.css('background', `linear-gradient(to right, #3498db ${percentage}%, #555 ${percentage}%)`);
 | 
						|
			} else if (seekBar) {
 | 
						|
				seekBar.val(0); // Reset if duration is not available
 | 
						|
			}
 | 
						|
		}
 | 
						|
 | 
						|
		function handleSeekBarInput() {
 | 
						|
			if (videoElement && seekBar) {
 | 
						|
				videoElement.currentTime = seekBar.val();
 | 
						|
			}
 | 
						|
		}
 | 
						|
 | 
						|
		function handleFullscreenChange() {
 | 
						|
			const isActuallyFullscreen = document.fullscreenElement || document.webkitIsFullScreen || document.mozFullScreen || document.msFullscreenElement;
 | 
						|
			if (!isActuallyFullscreen && overlayContainer && overlayContainer.is(':visible')) {
 | 
						|
				// If fullscreen exited and overlay was visible, treat as exiting video playback
 | 
						|
				// This helps if user presses Esc.
 | 
						|
				exitVideoPlayback();
 | 
						|
			}
 | 
						|
		}
 | 
						|
 | 
						|
        let videoInfo = [];
 | 
						|
        let selectedThumbnailIndex = 0;
 | 
						|
 | 
						|
        /**
 | 
						|
		 * Creates a new video thumbnail element.
 | 
						|
		 *
 | 
						|
		 * @param data The video data returned by the server.
 | 
						|
         * @param selected Set to true if this is the currently selected video thumbnail.
 | 
						|
         * @param append Set to true if this thumbnail should be appended to the video container, false if it should be prepended.
 | 
						|
         */
 | 
						|
        function createVideoThumbnailElement(data, selected, append) {
 | 
						|
			let thumbnail = $(`<img src="${data.thumbnail}" class="video-thumbnail" alt="Video Thumbnail" tabindex="0">`)[0];
 | 
						|
            let index;
 | 
						|
            if (append) {
 | 
						|
                $("#video-container").append(thumbnail);
 | 
						|
				index = videoInfo.length;
 | 
						|
                videoInfo.push({
 | 
						|
					thumbnail,
 | 
						|
					data
 | 
						|
                })
 | 
						|
            } else {
 | 
						|
				$("#video-container").prepend(thumbnail);
 | 
						|
				index = 0;
 | 
						|
				videoInfo.unshift({
 | 
						|
					thumbnail,
 | 
						|
					data
 | 
						|
				});
 | 
						|
                selectedThumbnailIndex++;
 | 
						|
			}
 | 
						|
            if (selected) {
 | 
						|
                thumbnail.classList.add("focussed");
 | 
						|
                selectedThumbnailIndex = index;
 | 
						|
            }
 | 
						|
		}
 | 
						|
 | 
						|
        /**
 | 
						|
		 * Selects a video thumbnail by its index into the videoThumbnails array.
 | 
						|
         * @param index The index of the thumbnail to select.
 | 
						|
         */
 | 
						|
        function selectThumbnail(index) {
 | 
						|
			if (index < 0 || index >= videoInfo.length) return; // Out of bounds check
 | 
						|
			if (selectedThumbnailIndex !== -1 && videoInfo[selectedThumbnailIndex]) {
 | 
						|
				videoInfo[selectedThumbnailIndex].thumbnail.classList.remove("focussed");
 | 
						|
			}
 | 
						|
			selectedThumbnailIndex = index;
 | 
						|
			videoInfo[selectedThumbnailIndex].thumbnail.classList.add("focussed");
 | 
						|
            for (let i = 0; i < videoInfo.length; i++) {
 | 
						|
				if (i < selectedThumbnailIndex - 1) {
 | 
						|
					videoInfo[i].thumbnail.style.left = "-20%";
 | 
						|
				} else if (i === selectedThumbnailIndex - 1) {
 | 
						|
                    videoInfo[i].thumbnail.style.left = "15%";
 | 
						|
				} else if (i === selectedThumbnailIndex) {
 | 
						|
					videoInfo[i].thumbnail.style.left = "50%";
 | 
						|
				} else if (i === selectedThumbnailIndex + 1) {
 | 
						|
					videoInfo[i].thumbnail.style.left = "85%";
 | 
						|
				} else {
 | 
						|
					videoInfo[i].thumbnail.style.left = "120%";
 | 
						|
				}
 | 
						|
			}
 | 
						|
		}
 | 
						|
 | 
						|
        /**
 | 
						|
		 * Starts the currently selected video.
 | 
						|
         */
 | 
						|
        function startVideo() {
 | 
						|
			showSpinner();
 | 
						|
 | 
						|
            let currentVideo = videoInfo[selectedThumbnailIndex].data
 | 
						|
 | 
						|
			// Clear previous handlers and set new ones for this specific load attempt
 | 
						|
			videoElement.oncanplaythrough = null;
 | 
						|
			videoElement.onerror = null;
 | 
						|
 | 
						|
			videoElement.oncanplaythrough = function() {
 | 
						|
				hideSpinner();
 | 
						|
				videoElement.oncanplaythrough = null; // Clean up handler
 | 
						|
				videoElement.onerror = null;       // Clean up handler
 | 
						|
				videoElement.play().catch(err => {
 | 
						|
					console.error("Error attempting to play video:", err);
 | 
						|
					exitVideoPlayback(); // Exit overlay on play error
 | 
						|
				});
 | 
						|
			};
 | 
						|
 | 
						|
			videoElement.onerror = function() {
 | 
						|
				console.error("Error loading video.");
 | 
						|
				hideSpinner();
 | 
						|
				videoElement.oncanplaythrough = null; // Clean up handler
 | 
						|
				videoElement.onerror = null;       // Clean up handler
 | 
						|
				exitVideoPlayback(); // Exit overlay on load error
 | 
						|
			};
 | 
						|
 | 
						|
			videoElement.src = currentVideo.url;
 | 
						|
			videoElement.load();
 | 
						|
 | 
						|
			// --- Setup Inactivity Controls (session-specific) ---
 | 
						|
			showControlsAndResetTimer(); // Initial call
 | 
						|
 | 
						|
			updatePlayPauseButtonState(); // Initial state for the button
 | 
						|
 | 
						|
 | 
						|
			// Show overlay and request fullscreen
 | 
						|
			overlayContainer.css('display', 'flex');
 | 
						|
			const containerEl = overlayContainer[0];
 | 
						|
			if (containerEl.requestFullscreen) {
 | 
						|
				containerEl.requestFullscreen().catch(err => console.error("Error attempting to enable full-screen mode:", err));
 | 
						|
			} else if (containerEl.mozRequestFullScreen) {
 | 
						|
				containerEl.mozRequestFullScreen();
 | 
						|
			} else if (containerEl.webkitRequestFullscreen) {
 | 
						|
				containerEl.webkitRequestFullscreen();
 | 
						|
			} else if (containerEl.msRequestFullscreen) {
 | 
						|
				containerEl.msRequestFullscreen();
 | 
						|
			}
 | 
						|
			// FOCUS: Set focus in the video overlay
 | 
						|
			if (playPauseButton && playPauseButton.length) {
 | 
						|
				playPauseButton.focus();
 | 
						|
			}
 | 
						|
            updatePlayPauseButtonState()
 | 
						|
		}
 | 
						|
 | 
						|
		$(document).ready(function() {
 | 
						|
			// Initialize global selectors for static elements
 | 
						|
			overlayContainer = $("#video-overlay-container");
 | 
						|
			videoElement = overlayContainer.find(".video-player")[0];
 | 
						|
			backButton = overlayContainer.find("#back-button");
 | 
						|
			customControls = overlayContainer.find("#custom-video-controls");
 | 
						|
			playPauseButton = customControls.find("#custom-play-pause-button");
 | 
						|
			seekBar = customControls.find("#custom-seek-bar");
 | 
						|
 | 
						|
			// Spinner is visible by default (CSS). Video container is hidden (CSS).
 | 
						|
			// Pre-set all thumbnails to a placeholder state
 | 
						|
			$(".video-thumbnail").attr("src", "data:image/svg+xml,%3Csvg xmlns=\\\'http://www.w3.org/2000/svg\\\' width=\\\'100%25\\\' height=\\\'100%25\\\' viewBox=\\\'0 0 16 9\\\'%3E%3C/svg%3E");
 | 
						|
 | 
						|
			$.getJSON("/api/homepage", function(data) {
 | 
						|
                createVideoThumbnailElement(data.currentVideo, true, true);
 | 
						|
                for (let i = 0; i < 10; i++)
 | 
						|
                    createVideoThumbnailElement(data.nextVideo, false, true);
 | 
						|
                for (let i = 0; i < 10; i++)
 | 
						|
                    createVideoThumbnailElement(data.nextVideo, false, false);
 | 
						|
				// currentVideo = data.currentVideo;
 | 
						|
                // nextVideo = data.nextVideo;
 | 
						|
				// $("#current-thumb").attr("src", currentVideo.thumbnail);
 | 
						|
                // $("#next-thumb").attr("src", nextVideo.thumbnail);
 | 
						|
				// Consider populating prev-thumb and next-thumb here if data provides them
 | 
						|
 | 
						|
				hideSpinner();
 | 
						|
                showVideoContainer();
 | 
						|
                selectThumbnail(selectedThumbnailIndex);
 | 
						|
			}).fail(function(jqXHR, textStatus, errorThrown) {
 | 
						|
                console.error("Failed to load homepage data:", textStatus, errorThrown);
 | 
						|
                hideSpinner();
 | 
						|
                // Display a user-friendly error message
 | 
						|
                if (!$('#init-error-msg').length) { // Prevent multiple error messages
 | 
						|
                    $('body').append('<div id="init-error-msg" style="color:white; text-align:center; padding:20px; position:fixed; top:50%; left:50%; transform:translate(-50%,-50%); background-color:rgba(0,0,0,0.8); border-radius:8px; z-index: 10002;">Failed to load content. Please try again later.</div>');
 | 
						|
                }
 | 
						|
            });
 | 
						|
 | 
						|
			// Attach event listeners that can be set up once for static controls
 | 
						|
			if (playPauseButton) playPauseButton.click(handlePlayPauseClick);
 | 
						|
			if (seekBar) seekBar.on('input', handleSeekBarInput);
 | 
						|
			if (backButton) backButton.click(exitVideoPlayback);
 | 
						|
 | 
						|
 | 
						|
            // --- Setup Custom Video Controls Events (session-specific for video element) ---
 | 
						|
            $(videoElement).on('play.currentVideo', updatePlayPauseButtonState);
 | 
						|
            $(videoElement).on('pause.currentVideo', updatePlayPauseButtonState);
 | 
						|
            $(videoElement).on('loadedmetadata.currentVideo', handleVideoLoadedMetadata);
 | 
						|
            $(videoElement).on('ended.currentVideo', exitVideoPlayback);
 | 
						|
            setInterval(handleVideoTimeUpdate, 1000/30);
 | 
						|
 | 
						|
            // --- Setup Fullscreen Change Listener (session-specific) ---
 | 
						|
            $(document).off('fullscreenchange.videoPlayback webkitfullscreenchange.videoPlayback mozFullScreen.videoPlayback MSFullscreenChange.videoPlayback', handleFullscreenChange);
 | 
						|
            $(document).on('fullscreenchange.videoPlayback webkitfullscreenchange.videoPlayback mozFullScreen.videoPlayback MSFullscreenChange.videoPlayback', handleFullscreenChange);
 | 
						|
 | 
						|
            overlayContainer.on('mousemove.inactivityControls', showControlsAndResetTimer);
 | 
						|
 | 
						|
            // --- TV Remote/Keyboard Navigation ---
 | 
						|
            const getOverlayViewFocusableElements = () => [backButton, playPauseButton, seekBar].filter(el => el && el.length > 0 && el.is(':visible'));
 | 
						|
 | 
						|
            $(document).on('keydown', function(e) {
 | 
						|
                let isOverlayVisible = overlayContainer.is(':visible');
 | 
						|
                let activeFocusableSet;
 | 
						|
 | 
						|
                if (isOverlayVisible) {
 | 
						|
                    activeFocusableSet = getOverlayViewFocusableElements();
 | 
						|
 | 
						|
                    const focusedElement = $(document.activeElement);
 | 
						|
                    let currentIndex = -1;
 | 
						|
 | 
						|
                    for (let i = 0; i < activeFocusableSet.length; i++) {
 | 
						|
                        if (focusedElement.is(activeFocusableSet[i])) {
 | 
						|
                            currentIndex = i;
 | 
						|
                            break;
 | 
						|
                        }
 | 
						|
                    }
 | 
						|
                }
 | 
						|
 | 
						|
                // Global key handling for overlay
 | 
						|
                if (isOverlayVisible) {
 | 
						|
                    switch (e.key || e.keyCode) {
 | 
						|
						case 'Backspace':
 | 
						|
						case 8:
 | 
						|
							e.preventDefault();
 | 
						|
							if (backButton && backButton.length) backButton.click();
 | 
						|
							return;
 | 
						|
						case 'Enter':
 | 
						|
						case 13:
 | 
						|
						case 179: // MediaPlayPause
 | 
						|
							e.preventDefault();
 | 
						|
							if (playPauseButton && playPauseButton.length) playPauseButton.click();
 | 
						|
							return;
 | 
						|
						case 178: // MediaStop
 | 
						|
							 e.preventDefault();
 | 
						|
							 exitVideoPlayback();
 | 
						|
							 return;
 | 
						|
						case 'ArrowLeft':
 | 
						|
						case 37: // ArrowLeft
 | 
						|
                            videoElement.currentTime = Math.max(0, videoElement.currentTime - 5); // Seek back 5s
 | 
						|
                            handleVideoTimeUpdate();
 | 
						|
                            showControlsAndResetTimer();
 | 
						|
                            e.preventDefault();
 | 
						|
                            return;
 | 
						|
						case 'ArrowRight':
 | 
						|
						case 39: // ArrowRight
 | 
						|
							videoElement.currentTime = Math.min(videoElement.duration, videoElement.currentTime + 5); // Seek forward 5s
 | 
						|
							handleVideoTimeUpdate();
 | 
						|
							showControlsAndResetTimer();
 | 
						|
							e.preventDefault();
 | 
						|
							return;
 | 
						|
                    }
 | 
						|
                }
 | 
						|
 | 
						|
                let keyHandled = false;
 | 
						|
                let nextIndex;
 | 
						|
                switch (e.key || e.keyCode) {
 | 
						|
                    case 'ArrowLeft':
 | 
						|
                    case 37: // ArrowLeft
 | 
						|
						nextIndex = Math.max(selectedThumbnailIndex - 1, 0);
 | 
						|
						selectThumbnail(nextIndex);
 | 
						|
                        keyHandled = true;
 | 
						|
                        break;
 | 
						|
 | 
						|
                    case 'ArrowRight':
 | 
						|
                    case 39: // ArrowRight
 | 
						|
						nextIndex = Math.min(selectedThumbnailIndex + 1, videoInfo.length - 1);
 | 
						|
						selectThumbnail(nextIndex);
 | 
						|
                        keyHandled = true;
 | 
						|
                        break;
 | 
						|
 | 
						|
                    case 'Enter':
 | 
						|
                    case 13: // Enter
 | 
						|
						startVideo();
 | 
						|
                        keyHandled = true;
 | 
						|
                        break;
 | 
						|
 | 
						|
                    case 179: // MediaPlayPause (if not handled by global overlay section)
 | 
						|
                        if (!isOverlayVisible && $('#play-button').length) {
 | 
						|
                           $('#play-button').click();
 | 
						|
                           keyHandled = true;
 | 
						|
                        }
 | 
						|
                        break;
 | 
						|
                }
 | 
						|
 | 
						|
                if (keyHandled) {
 | 
						|
                    e.preventDefault();
 | 
						|
                }
 | 
						|
            });
 | 
						|
		})
 | 
						|
	</script>
 | 
						|
</head>
 | 
						|
<body>
 | 
						|
	<div class="loader" id="loader"></div>
 | 
						|
	<div id="video-container">
 | 
						|
<!--		<img id="prev-thumb" class="video-thumbnail side-video" alt="Previous Video" tabindex="0">-->
 | 
						|
<!--		<img id="current-thumb" class="video-thumbnail current-video" alt="Current Video" tabindex="0">-->
 | 
						|
<!--		<img id="next-thumb" class="video-thumbnail side-video" alt="Next Video" tabindex="0">-->
 | 
						|
<!--		<div class="play-button" id="play-button" tabindex="0" role="button" aria-label="Play video"></div>-->
 | 
						|
	</div>
 | 
						|
 | 
						|
	<!-- STATIC VIDEO OVERLAY STRUCTURE -->
 | 
						|
	<div id="video-overlay-container">
 | 
						|
		<video class="video-player"></video> <!-- no src, no autoplay, no controls initially -->
 | 
						|
		<button id="back-button" tabindex="0">Back</button>
 | 
						|
		<div id="custom-video-controls">
 | 
						|
			<button id="custom-play-pause-button" class="nofocus" tabindex="0">►</button> <!-- Default to Play icon -->
 | 
						|
			<input type="range" id="custom-seek-bar" class="nofocus" value="0" step="any" tabindex="0" aria-label="Video seek bar" />
 | 
						|
		</div>
 | 
						|
	</div>
 | 
						|
</body>
 | 
						|
</html>
 |