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:
parent
18087c372d
commit
76ee56826f
134
index.html
134
index.html
|
|
@ -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');
|
||||
|
||||
|
|
|
|||
173
quick-play.html
173
quick-play.html
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Reference in New Issue