mirror of
https://github.com/DeNNiiInc/Web-Page-Performance-Test.git
synced 2026-04-17 20:05:58 +00:00
Optimize video generation: Fixed 30fps Whammy implementation
This commit is contained in:
178
main.js
178
main.js
@@ -630,137 +630,101 @@ async function downloadVideo() {
|
||||
|
||||
const downloadBtn = document.getElementById('video-download-btn');
|
||||
downloadBtn.disabled = true;
|
||||
downloadBtn.textContent = '⏳ Creating...';
|
||||
downloadBtn.textContent = '⏳ Compiling...';
|
||||
|
||||
try {
|
||||
// Create canvas for rendering at 1080p
|
||||
// Initialize Whammy video encoder with 30 FPS
|
||||
const fps = 30;
|
||||
const encoder = new Whammy(fps);
|
||||
|
||||
// Create canvas for high-quality rendering (1920x1080)
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = 1920;
|
||||
canvas.height = 1080;
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
// Try different codec options for browser compatibility
|
||||
const mimeTypes = [
|
||||
'video/webm;codecs=vp9',
|
||||
'video/webm;codecs=vp8',
|
||||
'video/webm',
|
||||
'video/mp4'
|
||||
];
|
||||
|
||||
let selectedMimeType = '';
|
||||
for (const mimeType of mimeTypes) {
|
||||
if (MediaRecorder.isTypeSupported(mimeType)) {
|
||||
selectedMimeType = mimeType;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!selectedMimeType) {
|
||||
throw new Error('No supported video format found');
|
||||
}
|
||||
|
||||
// Setup MediaRecorder with supported format and force high quality
|
||||
const stream = canvas.captureStream(30); // Increase to 30 FPS for smoother capture
|
||||
|
||||
// MediaRecorder options - force maximum quality
|
||||
const recorderOptions = {
|
||||
mimeType: selectedMimeType,
|
||||
videoBitsPerSecond: 75000000, // 75 Mbps
|
||||
bitsPerSecond: 75000000, // Some browsers use this instead
|
||||
};
|
||||
|
||||
// Add quality parameter if supported
|
||||
if (selectedMimeType.includes('webm')) {
|
||||
recorderOptions.videoKeyFrameIntervalDuration = 100; // Keyframe every 100ms for better quality
|
||||
}
|
||||
|
||||
const recorder = new MediaRecorder(stream, recorderOptions);
|
||||
|
||||
console.log('MediaRecorder configured with:', recorderOptions);
|
||||
console.log('Actual settings:', recorder.videoBitsPerSecond || 'not reported');
|
||||
|
||||
const chunks = [];
|
||||
recorder.ondataavailable = e => {
|
||||
if (e.data.size > 0) {
|
||||
chunks.push(e.data);
|
||||
}
|
||||
};
|
||||
|
||||
recorder.onstop = () => {
|
||||
const blob = new Blob(chunks, { type: selectedMimeType });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `page-load-${Date.now()}.webm`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
|
||||
downloadBtn.disabled = false;
|
||||
downloadBtn.textContent = '⬇ Download';
|
||||
};
|
||||
|
||||
recorder.onerror = (e) => {
|
||||
console.error('MediaRecorder error:', e);
|
||||
throw new Error('Recording failed');
|
||||
};
|
||||
|
||||
// Start with timeslice to ensure proper chunking and duration
|
||||
recorder.start(100); // Request data every 100ms
|
||||
|
||||
// Calculate frame duration based on filmstrip timing
|
||||
// Calculate total duration from the last frame's timing
|
||||
const totalDuration = videoFrames[videoFrames.length - 1].timing;
|
||||
const frameDuration = totalDuration / videoFrames.length;
|
||||
console.log(`Compiling video: ${videoFrames.length} source frames, total ${totalDuration}ms`);
|
||||
|
||||
console.log(`Rendering ${videoFrames.length} frames over ${totalDuration}ms (${frameDuration}ms per frame)`);
|
||||
// We will generate a frame for every 1/30th of a second
|
||||
const frameInterval = 1000 / fps; // ~33.33ms
|
||||
const totalOutputFrames = Math.ceil(totalDuration / frameInterval);
|
||||
|
||||
// Render frames with proper timing
|
||||
for (let i = 0; i < videoFrames.length; i++) {
|
||||
// Pre-load all images
|
||||
const loadedImages = await Promise.all(videoFrames.map(async frame => {
|
||||
const img = new Image();
|
||||
img.crossOrigin = 'anonymous';
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
img.onload = resolve;
|
||||
img.onerror = () => reject(new Error('Failed to load frame'));
|
||||
img.src = videoFrames[i].data;
|
||||
return new Promise((resolve, reject) => {
|
||||
img.onload = () => resolve({ img, timing: frame.timing });
|
||||
img.onerror = () => resolve(null); // Skip failed frames
|
||||
img.src = frame.data;
|
||||
});
|
||||
}));
|
||||
|
||||
// Render this frame multiple times to fill the duration
|
||||
const renderCount = Math.max(1, Math.ceil(frameDuration / 33)); // 33ms per render at 30fps
|
||||
const validImages = loadedImages.filter(i => i !== null);
|
||||
|
||||
for (let r = 0; r < renderCount; r++) {
|
||||
// Black background
|
||||
ctx.fillStyle = '#000';
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||
// Generate video frames
|
||||
for (let i = 0; i < totalOutputFrames; i++) {
|
||||
const currentTime = i * frameInterval;
|
||||
|
||||
// Center and scale image
|
||||
const scale = Math.min(canvas.width / img.width, canvas.height / img.height);
|
||||
const x = (canvas.width - img.width * scale) / 2;
|
||||
const y = (canvas.height - img.height * scale) / 2;
|
||||
ctx.drawImage(img, x, y, img.width * scale, img.height * scale);
|
||||
|
||||
// Add timestamp overlay
|
||||
ctx.fillStyle = 'rgba(0, 0, 0, 0.7)';
|
||||
ctx.fillRect(10, canvas.height - 50, 200, 40);
|
||||
ctx.fillStyle = '#fff';
|
||||
ctx.font = 'bold 24px Arial';
|
||||
ctx.fillText(`${(videoFrames[i].timing / 1000).toFixed(1)}s`, 20, canvas.height - 20);
|
||||
|
||||
// Wait for next frame at 30fps
|
||||
await new Promise(r => setTimeout(r, 33));
|
||||
// Find the image that should be displayed at this time
|
||||
// It's the latest image whose timing is <= currentTime
|
||||
let currentImage = validImages[0];
|
||||
for (let j = 0; j < validImages.length; j++) {
|
||||
if (validImages[j].timing <= currentTime) {
|
||||
currentImage = validImages[j];
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Draw frame
|
||||
// Clear and draw black background
|
||||
ctx.fillStyle = '#000';
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
// Center and scale image (Contain)
|
||||
const scale = Math.min(canvas.width / currentImage.img.width, canvas.height / currentImage.img.height);
|
||||
const x = (canvas.width - currentImage.img.width * scale) / 2;
|
||||
const y = (canvas.height - currentImage.img.height * scale) / 2;
|
||||
|
||||
// Use high quality image smoothing
|
||||
ctx.imageSmoothingEnabled = true;
|
||||
ctx.imageSmoothingQuality = 'high';
|
||||
ctx.drawImage(currentImage.img, x, y, currentImage.img.width * scale, currentImage.img.height * scale);
|
||||
|
||||
// Add timestamp overlay (crisp text)
|
||||
ctx.fillStyle = 'rgba(0, 0, 0, 0.7)';
|
||||
ctx.fillRect(20, canvas.height - 70, 220, 50);
|
||||
ctx.fillStyle = '#fff';
|
||||
ctx.font = 'bold 32px Arial';
|
||||
ctx.fillText(`${(currentTime / 1000).toFixed(1)}s`, 40, canvas.height - 35);
|
||||
|
||||
// Add frame to encoder
|
||||
encoder.add(canvas);
|
||||
|
||||
// Yield to UI thread occasionally to prevent freezing
|
||||
if (i % 15 === 0) await new Promise(r => setTimeout(r, 0));
|
||||
}
|
||||
|
||||
// Add a small delay before stopping to ensure all data is captured
|
||||
await new Promise(r => setTimeout(r, 200));
|
||||
// Compile and download
|
||||
const outputBlob = encoder.compile();
|
||||
const url = URL.createObjectURL(outputBlob);
|
||||
|
||||
// Stop recording after all frames are rendered
|
||||
recorder.stop();
|
||||
// Trigger download
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `page-load-${Date.now()}.webm`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Video download error:', error);
|
||||
alert(`Failed to create video: ${error.message}\n\nYour browser may not support this feature. Try using Chrome or Edge.`);
|
||||
} finally {
|
||||
downloadBtn.disabled = false;
|
||||
downloadBtn.textContent = '⬇ Download';
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user