UNPKG

@zyrecx/tflix

Version:

TFlix transforms Cineby.app into a TV-friendly experience for Samsung TVs running TizenBrew with enhanced remote navigation.

657 lines (557 loc) 18.4 kB
/*global navigate*/ import css from './ui.css'; let videoElement = null; let playerControls = null; let progressBar = null; let progressFilled = null; let hideControlsTimeout = null; /** * Initialize UI enhancements when DOM is loaded */ function initializeUI() { // Add CSS to head const style = document.createElement('style'); style.textContent = css; document.head.appendChild(style); // Enable navigation mode document.body.classList.add('tflix-navigation-mode'); // Initialize focus on a logical starting element initializeFocus(); // Setup event listeners for media control keys setupMediaControlListeners(); // Initialize video player enhancements when a video is played setupVideoPlayerObserver(); } /** * Find and set initial focus on a logical starting element */ function initializeFocus() { // Start by focusing on the first navigable element (like a menu item or featured content) const initialElements = [ // Main navigation elements document.querySelector('nav a'), document.querySelector('.navigation a'), document.querySelector('header a'), // Content cards/items document.querySelector('.movie-card'), document.querySelector('.content-item'), document.querySelector('.film-item'), // Fallback to any clickable element document.querySelector('a'), document.querySelector('button') ]; // Find the first valid element from our priority list const firstElement = initialElements.find(el => el !== null); if (firstElement) { firstElement.classList.add('tflix-focused'); firstElement.focus(); ensureElementIsVisible(firstElement); } } /** * Ensure the element is visible in the viewport * @param {HTMLElement} element - element to make visible */ function ensureElementIsVisible(element) { if (!element) return; element.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'nearest' }); } /** * Setup media control key event listeners */ function setupMediaControlListeners() { document.addEventListener('keydown', function(e) { // Handle media control keys switch (e.key) { case 'MediaPlayPause': togglePlayPause(); break; case 'MediaPlay': play(); break; case 'MediaPause': pause(); break; case 'MediaStop': stop(); break; case 'MediaFastForward': fastForward(); break; case 'MediaRewind': rewind(); break; case 'MediaTrackNext': // Jump forward 10 seconds seekRelative(10); break; case 'MediaTrackPrevious': // Jump backward 10 seconds seekRelative(-10); break; case 'Back': case 'XF86Back': // Handle back button press handleBackButton(e); break; } }); } /** * Handle back button press * @param {Event} e - The keydown event */ function handleBackButton(e) { e.preventDefault(); // Prevent default back behavior // Check if we're in a video player mode if (videoElement && videoElement.parentElement && (document.fullscreenElement || videoElement.closest('.video-player-container'))) { // If in fullscreen, exit it if (document.fullscreenElement) { document.exitFullscreen().catch(() => { // Silent error handling }); } // Stop video playback videoElement.pause(); // If there's a close button, click it const closeButton = document.querySelector('.close-button, .back-button, .exit-button'); if (closeButton) { closeButton.click(); } else { // Try to navigate back to the main content window.history.back(); } return; } // Handle regular navigation back if (window.history.length > 1) { window.history.back(); } else { // If no history, try to find a back button on the page const backButton = document.querySelector('.back-button, [aria-label="Back"], [aria-label="Go back"]'); if (backButton) { backButton.click(); } } } /** * Setup MutationObserver to detect when a video player is added to the DOM */ function setupVideoPlayerObserver() { // Create an observer instance const observer = new MutationObserver(function(mutations) { mutations.forEach(function(mutation) { if (mutation.addedNodes.length) { // Check if a video element was added const addedVideo = Array.from(mutation.addedNodes).find(node => node.nodeName === 'VIDEO' || (node.querySelector && node.querySelector('video')) ); if (addedVideo) { videoElement = addedVideo.nodeName === 'VIDEO' ? addedVideo : addedVideo.querySelector('video'); if (videoElement) { enhanceVideoPlayer(videoElement); } } } }); }); // Start observing the document body for DOM changes observer.observe(document.body, { childList: true, subtree: true }); // Also check for existing video elements videoElement = document.querySelector('video'); if (videoElement) { enhanceVideoPlayer(videoElement); } } /** * Enhance video player with custom controls and TV remote navigation * @param {HTMLElement} video - The video element to enhance */ function enhanceVideoPlayer(video) { videoElement = video; // Fix common video playback issues fixVideoPlaybackIssues(video); // Create custom player controls createPlayerControls(); // Add event listeners for video element videoElement.addEventListener('play', updatePlayerState); videoElement.addEventListener('pause', updatePlayerState); videoElement.addEventListener('timeupdate', updateProgress); videoElement.addEventListener('ended', onVideoEnded); // Add error handling videoElement.addEventListener('error', handleVideoError); // Show controls when moving focus with the TV remote document.addEventListener('keydown', function(e) { if (Object.values(ARROW_KEY_CODE).includes(e.key)) { showControls(); } }); } /** * Fix common video playback issues * @param {HTMLElement} video - The video element to fix */ function fixVideoPlaybackIssues(video) { if (!video) return; // Ensure video is visible video.style.display = 'block'; video.style.opacity = '1'; video.style.visibility = 'visible'; // Make sure the video container is visible const videoContainer = video.parentElement; if (videoContainer) { videoContainer.style.display = 'block'; videoContainer.style.opacity = '1'; videoContainer.style.visibility = 'visible'; videoContainer.style.backgroundColor = '#000'; // Black background // Add a specific class to help identify it videoContainer.classList.add('tflix-video-container'); // Fix position if it's absolute or fixed to make sure it's visible const containerStyle = window.getComputedStyle(videoContainer); if (containerStyle.position === 'absolute' || containerStyle.position === 'fixed') { videoContainer.style.top = '0'; videoContainer.style.left = '0'; videoContainer.style.width = '100%'; videoContainer.style.height = '100%'; videoContainer.style.zIndex = '9999'; } } // Ensure video can be played video.autoplay = true; video.controls = true; // Enable native controls as fallback // Try to fix video size video.style.width = '100%'; video.style.height = 'auto'; video.style.maxHeight = '100vh'; video.style.maxWidth = '100vw'; video.style.objectFit = 'contain'; // Ensure proper video rendering video.setAttribute('playsinline', ''); // Check for CORS issues and add crossorigin if needed if (!video.hasAttribute('crossorigin')) { video.setAttribute('crossorigin', 'anonymous'); } // If the TV has trouble with media codecs, try to help with hints if (!video.hasAttribute('preload')) { video.setAttribute('preload', 'auto'); } // Special handling for Cineby.app if (window.location.hostname.includes('cineby.app')) { // Make sure we can manipulate the video video.setAttribute('controlsList', 'nodownload'); // Store a reference for our Cineby-specific handlers window.tflixVideoElement = video; // Add event listeners for TV remote navigation during playback document.addEventListener('keydown', handleCinebyVideoKeyEvents); // Force a play attempt setTimeout(() => { video.play().catch(() => { // Silent error handling - we'll try to recover later if needed }); }, 1000); } } /** * Handle Cineby-specific video remote events * @param {Event} e - Remote control event */ function handleCinebyVideoKeyEvents(e) { const video = window.tflixVideoElement; if (!video) return; // Only process if we're on a video page and the video is visible if (!window.location.pathname.includes('/movie/') || video.style.display === 'none' || video.style.visibility === 'hidden') { return; } switch (e.key) { case 'Enter': e.preventDefault(); if (video.paused) { video.play(); } else { video.pause(); } showToast(video.paused ? 'Paused' : 'Playing'); break; case 'ArrowUp': e.preventDefault(); video.volume = Math.min(1, video.volume + 0.1); showToast(`Volume: ${Math.round(video.volume * 100)}%`); break; case 'ArrowDown': e.preventDefault(); video.volume = Math.max(0, video.volume - 0.1); showToast(`Volume: ${Math.round(video.volume * 100)}%`); break; case 'ArrowLeft': e.preventDefault(); video.currentTime = Math.max(0, video.currentTime - 10); showToast(`- 10 seconds`); break; case 'ArrowRight': e.preventDefault(); video.currentTime = Math.min(video.duration, video.currentTime + 10); showToast(`+ 10 seconds`); break; } } /** * Handle video playback errors * @param {Event} e - Error event */ function handleVideoError(e) { const errorMessage = getVideoErrorMessage(videoElement.error ? videoElement.error.code : 0); showToast(`Video error: ${errorMessage}. Trying to recover...`); // Store the current video source and position const currentSrc = videoElement.src; const currentTime = videoElement.currentTime || 0; // Special handling for Cineby.app if (window.location.hostname.includes('cineby.app')) { // For Cineby, try a more aggressive recovery approach // First, check if it's just a missing source or corruption if (!currentSrc || currentSrc === 'undefined' || currentSrc === '') { // Try to find another video element that might have a valid source const otherVideos = Array.from(document.querySelectorAll('video')).filter(v => v !== videoElement); if (otherVideos.length > 0) { for (const video of otherVideos) { if (video.src && video.src !== '') { videoElement.src = video.src; videoElement.load(); videoElement.currentTime = currentTime; videoElement.play().catch(() => { // If still fails, try reloading the page showToast('Still having trouble. Try using the back button and selecting again.'); }); return; } } } } } // Generic recovery approach setTimeout(() => { if (videoElement) { // Try reloading the video videoElement.src = ''; setTimeout(() => { videoElement.src = currentSrc; videoElement.load(); videoElement.currentTime = currentTime; videoElement.play().catch(() => { showToast('Could not play video. Try exiting and selecting again.'); }); }, 1000); } }, 2000); } /** * Get human-readable error message for video error code * @param {number} errorCode - The error code from video.error.code * @returns {string} Human-readable error message */ function getVideoErrorMessage(errorCode) { switch(errorCode) { case 1: return 'Fetching process aborted'; case 2: return 'Network error'; case 3: return 'Decoding error'; case 4: return 'Video not supported'; default: return 'Unknown error'; } } /** * Create custom player controls */ function createPlayerControls() { // First check if we already have controls if (playerControls) return; // Create controls container playerControls = document.createElement('div'); playerControls.className = 'tflix-player-controls'; // Create play/pause button const playPauseBtn = document.createElement('button'); playPauseBtn.className = 'tflix-control-button play-pause'; playPauseBtn.innerHTML = '⏸️'; playPauseBtn.addEventListener('click', togglePlayPause); // Create rewind button const rewindBtn = document.createElement('button'); rewindBtn.className = 'tflix-control-button rewind'; rewindBtn.innerHTML = '⏪'; rewindBtn.addEventListener('click', () => seekRelative(-10)); // Create fast-forward button const fastForwardBtn = document.createElement('button'); fastForwardBtn.className = 'tflix-control-button fast-forward'; fastForwardBtn.innerHTML = '⏩'; fastForwardBtn.addEventListener('click', () => seekRelative(10)); // Create progress bar progressBar = document.createElement('div'); progressBar.className = 'tflix-progress-bar'; progressFilled = document.createElement('div'); progressFilled.className = 'tflix-progress-filled'; progressBar.appendChild(progressFilled); // Add all elements to controls playerControls.appendChild(rewindBtn); playerControls.appendChild(playPauseBtn); playerControls.appendChild(progressBar); playerControls.appendChild(fastForwardBtn); // Find video container to append controls const videoContainer = videoElement.parentElement; if (videoContainer) { videoContainer.style.position = 'relative'; videoContainer.appendChild(playerControls); } else { // If we can't find a parent, append to body document.body.appendChild(playerControls); } } /** * Show player controls with auto-hide */ function showControls() { if (!playerControls) return; playerControls.classList.add('show'); // Clear any existing timeout if (hideControlsTimeout) { clearTimeout(hideControlsTimeout); } // Set timeout to hide controls after 3 seconds hideControlsTimeout = setTimeout(() => { playerControls.classList.remove('show'); }, 3000); } /** * Update player UI based on video state */ function updatePlayerState() { if (!videoElement || !playerControls) return; const playPauseBtn = playerControls.querySelector('.play-pause'); if (playPauseBtn) { playPauseBtn.innerHTML = videoElement.paused ? '▶️' : '⏸️'; } showControls(); } /** * Update progress bar */ function updateProgress() { if (!videoElement || !progressFilled) return; const percent = (videoElement.currentTime / videoElement.duration) * 100; progressFilled.style.width = `${percent}%`; } /** * Handle video ended event */ function onVideoEnded() { if (!playerControls) return; const playPauseBtn = playerControls.querySelector('.play-pause'); if (playPauseBtn) { playPauseBtn.innerHTML = '▶️'; } showControls(); } // Media control functions function togglePlayPause() { if (!videoElement) return; if (videoElement.paused) { videoElement.play(); } else { videoElement.pause(); } showToast(videoElement.paused ? 'Paused' : 'Playing'); } function play() { if (!videoElement) return; videoElement.play(); showToast('Playing'); } function pause() { if (!videoElement) return; videoElement.pause(); showToast('Paused'); } function stop() { if (!videoElement) return; videoElement.pause(); videoElement.currentTime = 0; showToast('Stopped'); } function fastForward() { seekRelative(30); } function rewind() { seekRelative(-30); } function seekRelative(seconds) { if (!videoElement) return; videoElement.currentTime = Math.max(0, Math.min( videoElement.duration, videoElement.currentTime + seconds )); showToast(`${seconds > 0 ? '+' : ''}${seconds} seconds`); } /** * Show a toast notification * @param {string} message - Message to display */ function showToast(message) { // Check if a toast already exists let toast = document.querySelector('.tflix-toast'); // If not, create one if (!toast) { toast = document.createElement('div'); toast.className = 'tflix-toast'; document.body.appendChild(toast); } // Update message and show toast.textContent = message; toast.classList.add('show'); // Hide after 2 seconds setTimeout(() => { toast.classList.remove('show'); }, 2000); } // Define arrow key codes globally for TV remote const ARROW_KEY_CODE = { 'ArrowLeft': 'left', 'ArrowUp': 'up', 'ArrowRight': 'right', 'ArrowDown': 'down' }; // Initialize UI when the page is loaded const interval = setInterval(() => { if (document.readyState === 'complete' || document.readyState === 'interactive') { try { initializeUI(); } catch (error) { // Try again in case of error setTimeout(initializeUI, 1000); } clearInterval(interval); } }, 250); // Register for back button events at the system level if available if (typeof tizen !== 'undefined' && tizen.tvinputdevice) { try { tizen.tvinputdevice.registerKey('Back'); } catch (e) { // Silent error handling } } export default { showToast };