/** * VideoCharm - Video Player * Handles video playback and interaction */ const VideoPlayer = (function() { // DOM Elements let videoFeed; let progressFill; let playPauseOverlay; let playIcon; let pauseIcon; let volumeControl; let volumeOnIcon; let volumeOffIcon; let likeButton; let favoriteButton; let likeCount; let favoriteCount; // State let videos = []; let currentIndex = 0; let touchStartY = 0; let touchEndY = 0; let isLoading = false; let isMuted = false; let videoLoadTimeout; /** * Initialize the video player */ function init() { // Get DOM elements videoFeed = document.getElementById('videoFeed'); progressFill = document.getElementById('progressFill'); playPauseOverlay = document.getElementById('playPauseOverlay'); playIcon = document.getElementById('playIcon'); pauseIcon = document.getElementById('pauseIcon'); volumeControl = document.getElementById('volumeControl'); volumeOnIcon = document.getElementById('volumeOnIcon'); volumeOffIcon = document.getElementById('volumeOffIcon'); likeButton = document.getElementById('likeButton'); favoriteButton = document.getElementById('favoriteButton'); likeCount = document.getElementById('likeCount'); favoriteCount = document.getElementById('favoriteCount'); // Set up event listeners setupEventListeners(); } /** * Set up event listeners for player controls */ function setupEventListeners() { // Touch swipe detection document.addEventListener('touchstart', handleTouchStart); document.addEventListener('touchend', handleTouchEnd); // Video feed click videoFeed.addEventListener('click', handleVideoClick); // Volume control volumeControl.addEventListener('click', toggleMute); // Action buttons likeButton.addEventListener('click', toggleLike); favoriteButton.addEventListener('click', toggleFavorite); } /** * Load videos from the API * @param {Number} count Number of videos to load * @param {Boolean} showLoading Whether to show loading screen * @returns {Promise} Promise that resolves when videos are loaded */ function loadVideos(count = 1, showLoading = true) { if (isLoading) return Promise.resolve(); isLoading = true; if (showLoading) { App.showLoading(); } // Clear any existing timeout if (videoLoadTimeout) { clearTimeout(videoLoadTimeout); } // Set a timeout to ensure loading doesn't hang videoLoadTimeout = setTimeout(() => { if (isLoading) { isLoading = false; if (showLoading) { App.hideLoading(); } App.showToast('加载超时,请检查网络后重试'); } }, 8000); return fetch(`/api/videos?count=${count}`) .then(response => { if (!response.ok) { throw new Error('Network response was not ok'); } return response.json(); }) .then(data => { if (data.success && data.videos && data.videos.length > 0) { // Immediately create and start playing the first video const firstVideo = data.videos[0]; createVideoElement(firstVideo.url); // If this is the first video, play it immediately if (videos.length === 1) { playCurrentVideo(); if (showLoading) { App.hideLoading(); } } // Asynchronously load remaining videos without blocking UI if (data.videos.length > 1) { setTimeout(() => { for (let i = 1; i < data.videos.length; i++) { createVideoElement(data.videos[i].url); } }, 300); } } else { throw new Error('No videos returned from API'); } }) .catch(error => { console.error('Error loading videos:', error); App.showToast('加载视频失败,请重试'); }) .finally(() => { clearTimeout(videoLoadTimeout); isLoading = false; if (showLoading) { App.hideLoading(); } }); } /** * Create a new video element * @param {String} videoUrl URL of the video */ function createVideoElement(videoUrl) { if (!videoUrl) return; // Create container const videoCard = document.createElement('div'); videoCard.className = 'video-card'; videoCard.style.transform = `translateY(${100 * videos.length}%)`; // Create video element const video = document.createElement('video'); video.src = videoUrl; video.loop = true; video.preload = 'auto'; video.muted = isMuted; video.playsInline = true; video.setAttribute('webkit-playsinline', ''); // Generate random data for the video const videoId = generateVideoId(); const videoTitle = `精彩视频 #${videos.length + 1}`; const videoDesc = getRandomDescription(); const randomLikes = Math.floor(Math.random() * 1000) + 1; // Create video object const videoObj = { id: videoId, url: videoUrl, title: videoTitle, desc: videoDesc, element: videoCard, video: video, likes: randomLikes, timestamp: new Date().getTime(), duration: 0 }; // Create video overlay with info const overlay = document.createElement('div'); overlay.className = 'video-overlay'; const videoInfo = document.createElement('div'); videoInfo.className = 'video-info'; const titleEl = document.createElement('div'); titleEl.className = 'video-title'; titleEl.textContent = videoObj.title; const descEl = document.createElement('div'); descEl.className = 'video-desc'; descEl.textContent = videoObj.desc; const metaEl = document.createElement('div'); metaEl.className = 'video-meta'; const durationEl = document.createElement('div'); durationEl.className = 'video-meta-item duration'; durationEl.innerHTML = ` 00:00 `; metaEl.appendChild(durationEl); videoInfo.appendChild(titleEl); videoInfo.appendChild(descEl); videoInfo.appendChild(metaEl); overlay.appendChild(videoInfo); // Add elements to DOM videoCard.appendChild(video); videoCard.appendChild(overlay); videoFeed.appendChild(videoCard); // Add to videos array videos.push(videoObj); // Set up video event listeners setupVideoEvents(video, videoObj, durationEl); } /** * Set up event listeners for a video * @param {HTMLElement} video Video element * @param {Object} videoObj Video object * @param {HTMLElement} durationEl Duration display element */ function setupVideoEvents(video, videoObj, durationEl) { // Use video's first frame as thumbnail when available video.addEventListener('loadeddata', function() { videoObj.hasLoadedData = true; // Capture first frame as thumbnail videoObj.thumbnail = captureVideoFrame(video); }); // Update duration when metadata is loaded video.addEventListener('loadedmetadata', function() { videoObj.duration = Math.round(video.duration); const durationSpan = durationEl.querySelector('span'); if (durationSpan) { durationSpan.textContent = formatDuration(videoObj.duration); } }); // Update progress bar video.addEventListener('timeupdate', function() { if (videos.indexOf(videoObj) === currentIndex) { const progress = (video.currentTime / video.duration) * 100; progressFill.style.width = `${progress}%`; } }); // Handle playback end (for non-looping videos) video.addEventListener('ended', function() { if (!video.loop) { // Automatically advance to next video switchToVideo(currentIndex + 1); } }); // Handle video errors video.addEventListener('error', function() { console.error('Video load error:', videoObj.url); App.showToast('视频加载失败,正在尝试下一个'); if (videos.indexOf(videoObj) === currentIndex) { switchToVideo(currentIndex + 1); } }); } /** * Capture a frame from video to use as thumbnail * @param {HTMLVideoElement} video Video element * @returns {String} Data URL of thumbnail */ function captureVideoFrame(video) { try { const canvas = document.createElement('canvas'); canvas.width = video.videoWidth; canvas.height = video.videoHeight; const ctx = canvas.getContext('2d'); ctx.drawImage(video, 0, 0, canvas.width, canvas.height); return canvas.toDataURL('image/jpeg', 0.7); } catch (e) { console.error('Error capturing video frame:', e); return null; } } /** * Switch to a different video * @param {Number} index Index of video to switch to */ function switchToVideo(index) { if (index < 0 || isLoading) return; // Handle case where requested index exceeds available videos if (index >= videos.length) { // Show loading toast and load more videos App.showToast('正在加载新视频...'); loadVideos(1, false).then(() => { // After loading, try again if video exists now if (index < videos.length) { switchToVideo(index); } }); return; } // Pause all videos videos.forEach(item => { item.video.pause(); }); // Update current index currentIndex = index; // Update video positions updateVideoPositions(); // Play current video playCurrentVideo(); // Reset progress bar progressFill.style.width = '0'; // Update action buttons updateActionButtons(); // Add to history if (videos[currentIndex]) { StorageManager.addToHistory(videos[currentIndex]); } // Preload more videos when close to end if (currentIndex >= videos.length - 1) { loadVideos(2, false); } } /** * Play the current video */ function playCurrentVideo() { if (!videos[currentIndex]) return; const video = videos[currentIndex].video; // Try to play and handle autoplay restrictions const playPromise = video.play(); if (playPromise !== undefined) { playPromise.catch(error => { console.log('Autoplay prevented, trying muted playback'); // Auto-play was prevented, set muted and try again video.muted = true; isMuted = true; updateVolumeUI(); video.play().catch(e => { console.error('Failed to play video even with mute:', e); // If even muted autoplay fails, show a play button overlay App.showToast('点击视频开始播放'); }); }); } } /** * Update positions of all videos */ function updateVideoPositions() { videos.forEach((item, idx) => { item.element.style.transform = `translateY(${(idx - currentIndex) * 100}%)`; }); } /** * Update action buttons state based on current video */ function updateActionButtons() { if (!videos[currentIndex]) return; const videoId = videos[currentIndex].id; // Update like button if (StorageManager.isLiked(videoId)) { likeButton.classList.add('active'); } else { likeButton.classList.remove('active'); } // Update favorite button if (StorageManager.isFavorite(videoId)) { favoriteButton.classList.add('active'); } else { favoriteButton.classList.remove('active'); } // Update counts likeCount.textContent = Math.floor(videos[currentIndex].likes); favoriteCount.textContent = StorageManager.isFavorite(videoId) ? '1' : '0'; } /** * Handle touch start event * @param {Event} e Touch event */ function handleTouchStart(e) { touchStartY = e.touches[0].clientY; } /** * Handle touch end event * @param {Event} e Touch event */ function handleTouchEnd(e) { // Only process touch events on home screen if (!document.getElementById('homeScreen').classList.contains('active')) { return; } touchEndY = e.changedTouches[0].clientY; const deltaY = touchStartY - touchEndY; // If swipe distance is significant if (Math.abs(deltaY) > 50) { if (deltaY > 0) { // Swipe up - next video switchToVideo(currentIndex + 1); } else { // Swipe down - previous video switchToVideo(currentIndex - 1); } } } /** * Handle click on video feed * @param {Event} e Click event */ function handleVideoClick(e) { // Ignore clicks on controls if (e.target.closest('.action-button') || e.target.closest('.volume-control')) { return; } // Toggle play/pause if (!videos[currentIndex]) return; const video = videos[currentIndex].video; if (video.paused) { video.play(); playIcon.style.display = 'block'; pauseIcon.style.display = 'none'; } else { video.pause(); playIcon.style.display = 'none'; pauseIcon.style.display = 'block'; } // Show play/pause overlay playPauseOverlay.classList.add('visible'); setTimeout(() => { playPauseOverlay.classList.remove('visible'); }, 800); } /** * Toggle mute state */ function toggleMute() { isMuted = !isMuted; // Update all videos videos.forEach(item => { item.video.muted = isMuted; }); // Update UI updateVolumeUI(); // Show toast App.showToast(isMuted ? '已静音' : '已开启声音'); } /** * Update volume control UI */ function updateVolumeUI() { if (isMuted) { volumeOnIcon.style.display = 'none'; volumeOffIcon.style.display = 'block'; } else { volumeOnIcon.style.display = 'block'; volumeOffIcon.style.display = 'none'; } } /** * Toggle like for current video */ function toggleLike() { if (!videos[currentIndex]) return; const videoId = videos[currentIndex].id; const newLikeState = StorageManager.toggleLike(videoId); // Update UI if (newLikeState) { likeButton.classList.add('active'); videos[currentIndex].likes++; App.showToast('已添加到喜欢'); } else { likeButton.classList.remove('active'); videos[currentIndex].likes--; App.showToast('已取消喜欢'); } // Update count likeCount.textContent = Math.floor(videos[currentIndex].likes); // Update profile stats App.updateStats(); } /** * Toggle favorite for current video */ function toggleFavorite() { if (!videos[currentIndex]) return; const newFavoriteState = StorageManager.toggleFavorite(videos[currentIndex]); // Update UI if (newFavoriteState) { favoriteButton.classList.add('active'); favoriteCount.textContent = '1'; App.showToast('已添加到收藏'); } else { favoriteButton.classList.remove('active'); favoriteCount.textContent = '0'; App.showToast('已取消收藏'); } // Update profile stats App.updateStats(); } /** * Play a video from the history or favorites * @param {Object} videoData Video data from storage */ function playVideoFromLibrary(videoData) { if (!videoData || !videoData.url) return; // Check if video is already loaded const existingIndex = videos.findIndex(v => v.id === videoData.id); if (existingIndex !== -1) { // Switch to existing video switchToVideo(existingIndex); } else { // Create new video element createVideoElement(videoData.url); // Switch to the new video switchToVideo(videos.length - 1); } // Switch to home screen App.switchToScreen('homeScreen'); } /** * Generate a unique video ID * @returns {String} Unique ID */ function generateVideoId() { return 'v_' + Math.random().toString(36).substr(2, 9) + '_' + Date.now(); } /** * Format duration in seconds to MM:SS * @param {Number} seconds Duration in seconds * @returns {String} Formatted duration */ function formatDuration(seconds) { if (!seconds) return '00:00'; const minutes = Math.floor(seconds / 60); const remainingSeconds = Math.floor(seconds % 60); return `${minutes < 10 ? '0' : ''}${minutes}:${remainingSeconds < 10 ? '0' : ''}${remainingSeconds}`; } /** * Get random video description * @returns {String} Random description */ function getRandomDescription() { const descriptions = [ "精选高质量视频,带给你视觉享受", "每日精选,让你流连忘返", "热门推荐,不容错过的精彩瞬间", "探索更多精彩内容,尽在VideoCharm", "发现生活中的美好瞬间", "精彩纷呈,尽在眼前", "让心情放松的精选内容", "视觉盛宴,触手可及" ]; return descriptions[Math.floor(Math.random() * descriptions.length)]; } // Public API return { init: init, loadVideos: loadVideos, switchToVideo: switchToVideo, playVideoFromLibrary: playVideoFromLibrary, updateVideoPositions: updateVideoPositions }; })();