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