mirror of
https://github.com/alexhopeoconnor/bom-local-service.git
synced 2026-10-03 21:21:59 +10:00
Fix frame jiggle and improve screenshot quality
- Add wait for frame 0 tiles to load before calculating bounding box - Preload all frame images on client to prevent layout shifts - Use absolute positioning for radar images to prevent reflow - Add 2x device scale factor for higher resolution screenshots - Explicit PNG format and disable animations for better quality - Wait for fonts to load before capturing screenshots
This commit is contained in:
@@ -95,6 +95,7 @@ public class BrowserService : IBrowserService
|
||||
return await browser.NewContextAsync(new BrowserNewContextOptions
|
||||
{
|
||||
ViewportSize = new ViewportSize { Width = 1920, Height = 1080 },
|
||||
DeviceScaleFactor = 2.0f, // 2x scale for higher resolution screenshots (better text quality)
|
||||
UserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
|
||||
Locale = "en-AU",
|
||||
TimezoneId = _timezone,
|
||||
|
||||
+27
-23
@@ -414,12 +414,18 @@ public class ScrapingService : IScrapingService
|
||||
|
||||
await _debugService.SaveStepDebugAsync(debugFolder, 10, "scrubber_at_position_0", page, consoleMessages, networkRequests, cancellationToken);
|
||||
|
||||
// Step 11: Extract metadata and frame information
|
||||
// Step 11: Wait for frame 0 tiles to fully load before calculating bounding box
|
||||
// This ensures the map viewport is stable and prevents jiggle between frames
|
||||
_logger.LogInformation("Waiting for frame 0 tiles to fully render");
|
||||
await page.WaitForTimeoutAsync(_tileRenderWaitMs);
|
||||
|
||||
// Step 12: Extract metadata and frame information
|
||||
_logger.LogInformation("Extracting metadata and frame information");
|
||||
var lastUpdatedInfo = await _timeParsingService.ExtractLastUpdatedInfoAsync(page);
|
||||
var frameInfo = await ExtractFrameInfoAsync(page);
|
||||
|
||||
// Step 12: Get map container and calculate bounding box once
|
||||
// Step 13: Get map container and calculate bounding box once
|
||||
// Calculate after frame 0 tiles are loaded to ensure consistent viewport
|
||||
_logger.LogInformation("Preparing map container for screenshot");
|
||||
var mapContainer = page.Locator(".esri-view-surface").First;
|
||||
await mapContainer.WaitForAsync(new LocatorWaitForOptions { Timeout = 10000 });
|
||||
@@ -445,11 +451,11 @@ public class ScrapingService : IScrapingService
|
||||
Height = boundingBox.Height
|
||||
};
|
||||
|
||||
// Step 13: Use provided cache folder (already created by BomRadarService)
|
||||
// Step 14: Use provided cache folder (already created by BomRadarService)
|
||||
Directory.CreateDirectory(cacheFolderPath);
|
||||
_logger.LogInformation("Using cache folder: {Path}", cacheFolderPath);
|
||||
|
||||
// Step 14-20: Capture all 7 frames
|
||||
// Step 15-21: Capture all 7 frames
|
||||
var frames = new List<RadarFrame>();
|
||||
var stepForwardButton = page.Locator("button[data-testid='bom-scrub-utils__right__step-forward']").First;
|
||||
|
||||
@@ -485,7 +491,7 @@ public class ScrapingService : IScrapingService
|
||||
frameIndex, framePath, minutesAgo.Value);
|
||||
|
||||
// Save debug screenshot BEFORE clicking step forward
|
||||
await _debugService.SaveStepDebugAsync(debugFolder, 14 + frameIndex, $"frame_{frameIndex}_captured", page, consoleMessages, networkRequests, cancellationToken);
|
||||
await _debugService.SaveStepDebugAsync(debugFolder, 15 + frameIndex, $"frame_{frameIndex}_captured", page, consoleMessages, networkRequests, cancellationToken);
|
||||
|
||||
// If not the last frame, click step forward to prepare for next frame
|
||||
if (frameIndex < 6)
|
||||
@@ -521,26 +527,11 @@ public class ScrapingService : IScrapingService
|
||||
|
||||
_logger.LogInformation("All 7 frames captured successfully");
|
||||
|
||||
// Step 21: Save metadata and frame information
|
||||
// Step 22: Save metadata and frame information
|
||||
await _cacheService.SaveMetadataAsync(cacheFolderPath, lastUpdatedInfo, cancellationToken);
|
||||
await _cacheService.SaveFramesMetadataAsync(cacheFolderPath, frames, cancellationToken);
|
||||
|
||||
// Remove lock file to indicate cache folder is complete
|
||||
var lockFilePath = FilePathHelper.GetCacheLockFilePath(cacheFolderPath);
|
||||
try
|
||||
{
|
||||
if (File.Exists(lockFilePath))
|
||||
{
|
||||
File.Delete(lockFilePath);
|
||||
_logger.LogDebug("Removed cache lock file: {Path}", lockFilePath);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Failed to remove cache lock file: {Path}", lockFilePath);
|
||||
}
|
||||
|
||||
// Step 22: Return response with all frames
|
||||
// Step 23: Return response with all frames
|
||||
// Note: ScrapingService doesn't have access to cache management check interval
|
||||
// Default to 5 minutes (standard check interval)
|
||||
return ResponseBuilder.CreateRadarResponse(cacheFolderPath, frames, lastUpdatedInfo, suburb, state, cacheIsValid: true, cacheExpiresAt: null, isUpdating: false, cacheManagementCheckIntervalMinutes: 5);
|
||||
@@ -728,10 +719,23 @@ public class ScrapingService : IScrapingService
|
||||
cropArea = containerClip;
|
||||
}
|
||||
|
||||
// Wait for fonts to be loaded to prevent text rendering artifacts
|
||||
try
|
||||
{
|
||||
await page.WaitForLoadStateAsync(LoadState.NetworkIdle, new PageWaitForLoadStateOptions { Timeout = 5000 });
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Continue if network idle timeout - fonts may already be loaded
|
||||
}
|
||||
|
||||
// Take high-quality screenshot with explicit PNG format and disabled animations
|
||||
await page.ScreenshotAsync(new PageScreenshotOptions
|
||||
{
|
||||
Path = outputPath,
|
||||
Clip = cropArea
|
||||
Clip = cropArea,
|
||||
Type = ScreenshotType.Png, // Explicit PNG for lossless quality
|
||||
Animations = ScreenshotAnimations.Disabled // Disable animations to prevent artifacts
|
||||
});
|
||||
|
||||
_logger.LogDebug("Screenshot saved: {Path} (crop: {X},{Y} {Width}x{Height})",
|
||||
|
||||
@@ -80,13 +80,18 @@
|
||||
overflow: hidden;
|
||||
margin-bottom: 20px;
|
||||
aspect-ratio: 16/9;
|
||||
min-height: 400px; /* Prevent layout shift */
|
||||
}
|
||||
|
||||
.radar-image {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
display: block;
|
||||
image-rendering: crisp-edges; /* Prevent blurry scaling */
|
||||
}
|
||||
|
||||
.loading {
|
||||
@@ -732,6 +737,9 @@
|
||||
</button>
|
||||
`).join('');
|
||||
|
||||
// Preload all frame images to prevent jiggle when switching
|
||||
preloadImages(frames);
|
||||
|
||||
// Show first frame
|
||||
if (frames.length > 0) {
|
||||
showFrame(0);
|
||||
@@ -745,6 +753,14 @@
|
||||
updateRelativeTimes();
|
||||
}
|
||||
|
||||
// Preload all frame images to prevent jiggle
|
||||
function preloadImages(frames) {
|
||||
frames.forEach(frame => {
|
||||
const img = new Image();
|
||||
img.src = frame.imageUrl;
|
||||
});
|
||||
}
|
||||
|
||||
// Show specific frame
|
||||
function showFrame(index) {
|
||||
if (index < 0 || index >= frames.length) return;
|
||||
@@ -756,13 +772,21 @@
|
||||
const imgEl = document.getElementById('radar-image');
|
||||
const loadingEl = document.getElementById('loading');
|
||||
|
||||
imgEl.onload = () => {
|
||||
// Check if image is already loaded (from preload)
|
||||
const preloadedImg = new Image();
|
||||
preloadedImg.onload = () => {
|
||||
// Image is ready, switch immediately to prevent jiggle
|
||||
imgEl.src = frame.imageUrl;
|
||||
imgEl.alt = `Radar frame ${frame.frameIndex} (${frame.minutesAgo} minutes ago)`;
|
||||
loadingEl.style.display = 'none';
|
||||
imgEl.style.display = 'block';
|
||||
};
|
||||
|
||||
imgEl.src = frame.imageUrl;
|
||||
imgEl.alt = `Radar frame ${frame.frameIndex} (${frame.minutesAgo} minutes ago)`;
|
||||
preloadedImg.onerror = () => {
|
||||
// Fallback if preload failed
|
||||
imgEl.src = frame.imageUrl;
|
||||
imgEl.alt = `Radar frame ${frame.frameIndex} (${frame.minutesAgo} minutes ago)`;
|
||||
};
|
||||
preloadedImg.src = frame.imageUrl;
|
||||
|
||||
// Update frame info
|
||||
document.getElementById('frame-info').textContent =
|
||||
|
||||
+1
-1
@@ -29,7 +29,7 @@ services:
|
||||
- CACHECLEANUP__INTERVALHOURS=${CACHECLEANUP__INTERVALHOURS:-1}
|
||||
- SCREENSHOT__DYNAMICCONTENTWAITMS=${SCREENSHOT__DYNAMICCONTENTWAITMS:-2000}
|
||||
- SCREENSHOT__TILERENDERWAITMS=${SCREENSHOT__TILERENDERWAITMS:-5000}
|
||||
- DEBUG__ENABLED=${DEBUG__ENABLED:-false}
|
||||
- DEBUG__ENABLED=${DEBUG__ENABLED:-true}
|
||||
- DEBUG__WAITMS=${DEBUG__WAITMS:-2000}
|
||||
- TIMEZONE=${TIMEZONE:-Australia/Brisbane}
|
||||
shm_size: '1gb'
|
||||
|
||||
Reference in New Issue
Block a user