FIX: Video storage quota and add videos to photo gallery

FIXED: Storage quota exceeded error
- Reduced video recording bitrate to 250kbps for smaller file sizes
- Added fallback recording options for better browser compatibility
- Implemented thumbnail-only storage for large videos with immediate download option
- Limited stored videos to 5 to prevent quota issues

ADDED: Videos to photo gallery (Gallery tab)
- Integrated captured videos into setupLibraryGalleryTab() alongside photos
- Videos appear in main gallery with photo-like interface and thumbnails
- Added proper video thumbnail generation and display
- Included video duration and metadata in gallery view

IMPLEMENTED: Smart storage management
- Videos stored with thumbnail for gallery display + full video for playback/download
- Graceful fallback when storage quota exceeded (thumbnail only + immediate download)
- Separate delete functions for gallery vs video library
- Updated gallery count to show 'X photos, Y videos'

ENHANCED: Video playback and download
- Updated video playback to use stored videoBlob with fallback messaging
- Download function checks for video availability before attempting
- Added hover effects for video thumbnails with play overlay
- Proper error handling for missing or corrupted video data

RESULT: Videos now appear in photo gallery like photos
- Users can view video thumbnails alongside photos in Gallery tab
- Efficient storage prevents quota errors while maintaining functionality
- Seamless integration with existing photo gallery interface
This commit is contained in:
dilgenfritz 2025-11-10 11:03:56 -06:00
parent 18087c372d
commit 76ee56826f
2 changed files with 239 additions and 68 deletions

View File

@ -4328,8 +4328,53 @@
}
});
// Add captured videos to the gallery
const capturedVideos = JSON.parse(localStorage.getItem('capturedVideos') || '[]');
console.log(`📹 Found ${capturedVideos.length} captured videos`);
capturedVideos.forEach((video, index) => {
const timestamp = new Date(video.timestamp || Date.now()).toLocaleDateString();
const duration = formatVideoDuration(video.duration);
photosHtml += `
<div class="photo-item video-item" data-video-id="${video.id}">
<div class="photo-container">
<div class="photo-checkbox">
<input type="checkbox" id="video-${index}" class="video-select" data-video-id="${video.id}" onchange="updateSelectionCount()">
<label for="video-${index}" class="checkbox-label"></label>
</div>
<div class="video-thumbnail-wrapper" onclick="playCapturedVideo('${video.id}')"
style="position: relative; cursor: pointer;"
onmouseover="this.querySelector('.video-play-overlay').style.opacity='1'"
onmouseout="this.querySelector('.video-play-overlay').style.opacity='0'">
${video.thumbnail ?
`<img src="${video.thumbnail}" alt="Video Thumbnail" style="width: 100%; height: 120px; object-fit: cover; border-radius: 8px;">` :
`<div style="width: 100%; height: 120px; background: #333; display: flex; align-items: center; justify-content: center; border-radius: 8px; color: #fff;">📹</div>`
}
<div class="video-play-overlay" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; background: rgba(0,0,0,0.5); opacity: 0; transition: opacity 0.3s; border-radius: 8px;">
<div class="play-icon-small" style="color: white; font-size: 24px;">▶️</div>
</div>
</div>`
<div class="photo-actions">
<button class="photo-download-btn" onclick="downloadCapturedVideo('${video.id}')" title="Download Video">
📥
</button>
<button class="photo-delete-btn" onclick="deleteCapturedVideoFromGallery('${video.id}')" title="Delete Video">
🗑️
</button>
</div>
<div class="photo-info">
<span class="photo-date">${timestamp}</span>
<span class="photo-type">Session Video • ${duration}</span>
</div>
</div>
</div>
`;
});
allPhotosGrid.innerHTML = photosHtml;
if (allPhotosCount) allPhotosCount.textContent = `${capturedPhotos.length} photos`;
const totalItems = capturedPhotos.length + capturedVideos.length;
if (allPhotosCount) allPhotosCount.textContent = `${capturedPhotos.length} photos, ${capturedVideos.length} videos`;
}
}
@ -4608,6 +4653,11 @@
return;
}
if (!video.videoBlob) {
alert('📹 Full video not available. This recording was too large to store completely. You can only view the thumbnail.');
return;
}
// Create fullscreen video player
const overlay = document.createElement('div');
overlay.className = 'video-player-overlay';
@ -4620,7 +4670,7 @@
overlay.innerHTML = `
<div class="video-player-container" style="position: relative; max-width: 90vw; max-height: 90vh;">
<video controls autoplay style="width: 100%; height: auto; max-height: 90vh; border-radius: 10px;">
<source src="${video.dataURL}" type="video/webm">
<source src="${video.videoBlob}" type="video/webm">
</video>
<button class="close-player" onclick="closeCapturedVideoPlayer()"
style="position: absolute; top: -40px; right: 0; background: rgba(255,255,255,0.2);
@ -4649,8 +4699,13 @@
return;
}
if (!video.videoBlob) {
alert('📹 Full video not available for download. This recording was too large to store completely.');
return;
}
const a = document.createElement('a');
a.href = video.dataURL;
a.href = video.videoBlob;
a.download = `quick-play-session-${new Date(video.timestamp).toISOString().slice(0, 19).replace(/:/g, '-')}.webm`;
document.body.appendChild(a);
@ -4684,6 +4739,27 @@
}
}
function deleteCapturedVideoFromGallery(videoId) {
if (!confirm('⚠️ Are you sure you want to delete this video? This cannot be undone.')) {
return;
}
let capturedVideos = JSON.parse(localStorage.getItem('capturedVideos') || '[]');
capturedVideos = capturedVideos.filter(v => v.id !== videoId);
localStorage.setItem('capturedVideos', JSON.stringify(capturedVideos));
console.log('🗑️ Video deleted from gallery:', videoId);
// Refresh both the gallery and video library
setupLibraryGalleryTab();
setupLibraryVideoTab();
if (window.game && window.game.flashMessageManager) {
window.game.flashMessageManager.show('🗑️ Video deleted successfully!', 'info');
}
}
// Initialize bulk action event listeners
function initializeBulkActions() {
const selectAllBtn = document.getElementById('select-all-photos');
@ -5966,35 +6042,33 @@
const capturedVideos = JSON.parse(localStorage.getItem('capturedVideos') || '[]');
console.log(`📹 Found ${capturedVideos.length} captured session videos`);
capturedVideos.forEach((video, index) => {
const date = new Date(video.timestamp);
const duration = formatVideoDuration(video.duration);
const videoElement = document.createElement('div');
videoElement.className = 'gallery-item captured-video-item';
videoElement.innerHTML = `
<div class="video-thumbnail-container">
<video class="video-thumbnail" preload="metadata" muted>
<source src="${video.dataURL}" type="video/webm">
</video>
<div class="video-overlay">
<div class="play-icon">▶️</div>
capturedVideos.forEach((video, index) => {
const date = new Date(video.timestamp);
const duration = formatVideoDuration(video.duration);
const videoElement = document.createElement('div');
videoElement.className = 'gallery-item captured-video-item';
videoElement.innerHTML = `
<div class="video-thumbnail-container">
${video.thumbnail ?
`<img class="video-thumbnail" src="${video.thumbnail}" alt="Video Thumbnail" style="width: 100%; height: 150px; object-fit: cover;">` :
`<div class="video-fallback" style="display: flex; width: 100%; height: 150px; background: #333; align-items: center; justify-content: center;">
<div class="video-icon">📹</div>
</div>`
}
<div class="video-overlay">
<div class="play-icon">▶️</div>
</div>
</div>
<div class="video-fallback" style="display: none;">
<div class="video-icon">📹</div>
<div class="video-info">
<div class="video-name">Session Recording ${index + 1}</div>
<div class="video-directory">Captured • ${date.toLocaleDateString()} • ${duration}</div>
</div>
</div>
<div class="video-info">
<div class="video-name">Session Recording ${index + 1}</div>
<div class="video-directory">Captured • ${date.toLocaleDateString()} • ${duration}</div>
</div>
<div class="video-actions">
<button class="btn-small" onclick="playCapturedVideo('${video.id}')">▶️ Play</button>
<button class="btn-small" onclick="downloadCapturedVideo('${video.id}')">💾 Download</button>
<button class="btn-small btn-danger" onclick="deleteCapturedVideo('${video.id}')">🗑️ Delete</button>
</div>
`;
// Handle video thumbnail loading
<div class="video-actions">
<button class="btn-small" onclick="playCapturedVideo('${video.id}')">▶️ Play</button>
<button class="btn-small" onclick="downloadCapturedVideo('${video.id}')">💾 Download</button>
<button class="btn-small btn-danger" onclick="deleteCapturedVideo('${video.id}')">🗑️ Delete</button>
</div>
`; // Handle video thumbnail loading
const videoThumb = videoElement.querySelector('.video-thumbnail');
const fallback = videoElement.querySelector('.video-fallback');

View File

@ -4464,11 +4464,21 @@
// Apply user settings
webcamViewer.className = `webcam-viewer active ${quickPlaySettings.webcamSize} ${quickPlaySettings.webcamPosition}`;
// Set up recording
// Set up recording with lower quality for storage efficiency
recordedChunks = [];
mediaRecorder = new MediaRecorder(webcamStream, {
mimeType: 'video/webm'
});
const options = {
mimeType: 'video/webm;codecs=vp8',
videoBitsPerSecond: 250000 // 250kbps for smaller file size
};
// Try different recording options if not supported
if (MediaRecorder.isTypeSupported('video/webm;codecs=vp8')) {
mediaRecorder = new MediaRecorder(webcamStream, options);
} else if (MediaRecorder.isTypeSupported('video/webm')) {
mediaRecorder = new MediaRecorder(webcamStream, { mimeType: 'video/webm' });
} else {
mediaRecorder = new MediaRecorder(webcamStream);
}
mediaRecorder.ondataavailable = (event) => {
if (event.data.size > 0) {
@ -4590,43 +4600,130 @@
const reader = new FileReader();
reader.onload = function(event) {
const videoData = {
id: `session_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
dataURL: event.target.result,
timestamp: Date.now(),
sessionType: 'quick-play-recording',
duration: Date.now() - sessionStats.started, // Session duration
metadata: {
type: 'session-recording',
format: 'webm',
source: 'quick-play',
settings: {
position: quickPlaySettings.webcamPosition,
size: quickPlaySettings.webcamSize
// Create video element to capture thumbnail
const video = document.createElement('video');
video.src = event.target.result;
video.muted = true;
video.currentTime = 2; // Seek to 2 seconds for thumbnail
video.addEventListener('seeked', function() {
// Create canvas to capture thumbnail
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
canvas.width = 160; // Small thumbnail size
canvas.height = 90; // 16:9 aspect ratio
ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
const thumbnailDataURL = canvas.toDataURL('image/jpeg', 0.7); // Compressed thumbnail
const videoData = {
id: `session_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
thumbnail: thumbnailDataURL, // Store only thumbnail, not full video
videoBlob: event.target.result, // Store full video separately for download
timestamp: Date.now(),
sessionType: 'quick-play-recording',
duration: Date.now() - sessionStats.started,
metadata: {
type: 'session-recording',
format: 'webm',
source: 'quick-play',
settings: {
position: quickPlaySettings.webcamPosition,
size: quickPlaySettings.webcamSize
}
}
};
try {
// Try to save - if it fails due to quota, save without full video
let capturedVideos = JSON.parse(localStorage.getItem('capturedVideos') || '[]');
capturedVideos.push(videoData);
// Keep only last 5 recordings to manage storage
if (capturedVideos.length > 5) {
capturedVideos = capturedVideos.slice(-5);
}
localStorage.setItem('capturedVideos', JSON.stringify(capturedVideos));
console.log('✅ Session recording saved with full video:', videoData.id);
} catch (error) {
console.warn('⚠️ Full video too large for storage, saving thumbnail only:', error);
// Fallback: Save without full video data
const lightVideoData = {
...videoData,
videoBlob: null, // Remove full video data
storageError: true
};
try {
let capturedVideos = JSON.parse(localStorage.getItem('capturedVideos') || '[]');
capturedVideos.push(lightVideoData);
if (capturedVideos.length > 10) {
capturedVideos = capturedVideos.slice(-10);
}
localStorage.setItem('capturedVideos', JSON.stringify(capturedVideos));
console.log('✅ Session recording saved (thumbnail only):', lightVideoData.id);
// Offer immediate download since storage failed
if (confirm('📹 Video too large for gallery storage. Download now?')) {
const a = document.createElement('a');
a.href = event.target.result;
a.download = `quick-play-session-${Date.now()}.webm`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
}
} catch (secondError) {
console.error('❌ Failed to save even thumbnail:', secondError);
// Last resort: offer immediate download
const a = document.createElement('a');
a.href = event.target.result;
a.download = `quick-play-session-${Date.now()}.webm`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
}
}
};
// Show completion message
if (window.flashMessageManager) {
window.flashMessageManager.show('📹 Session recording saved! Check library gallery to view.', 'positive');
}
// Clean up
recordedChunks = [];
video.remove();
});
// Save to localStorage gallery (same pattern as photos)
let capturedVideos = JSON.parse(localStorage.getItem('capturedVideos') || '[]');
capturedVideos.push(videoData);
// Keep only last 10 recordings to manage storage
if (capturedVideos.length > 10) {
capturedVideos = capturedVideos.slice(-10);
}
localStorage.setItem('capturedVideos', JSON.stringify(capturedVideos));
console.log('✅ Session recording saved to gallery:', videoData.id);
// Show completion message
if (window.flashMessageManager) {
window.flashMessageManager.show('📹 Session recording saved to gallery! Check your session videos to download.', 'positive');
}
// Clean up
recordedChunks = [];
video.addEventListener('error', function() {
console.warn('⚠️ Could not create thumbnail, saving without thumbnail');
// Fallback without thumbnail
const videoData = {
id: `session_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
thumbnail: null,
videoBlob: event.target.result,
timestamp: Date.now(),
sessionType: 'quick-play-recording',
duration: Date.now() - sessionStats.started,
metadata: {
type: 'session-recording',
format: 'webm',
source: 'quick-play',
settings: {
position: quickPlaySettings.webcamPosition,
size: quickPlaySettings.webcamSize
}
}
};
// Same storage logic as above...
recordedChunks = [];
});
};
reader.readAsDataURL(blob);