mirror of
https://github.com/alexhopeoconnor/bom-local-service.git
synced 2026-10-04 03:28:11 +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
|
return await browser.NewContextAsync(new BrowserNewContextOptions
|
||||||
{
|
{
|
||||||
ViewportSize = new ViewportSize { Width = 1920, Height = 1080 },
|
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",
|
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",
|
Locale = "en-AU",
|
||||||
TimezoneId = _timezone,
|
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);
|
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");
|
_logger.LogInformation("Extracting metadata and frame information");
|
||||||
var lastUpdatedInfo = await _timeParsingService.ExtractLastUpdatedInfoAsync(page);
|
var lastUpdatedInfo = await _timeParsingService.ExtractLastUpdatedInfoAsync(page);
|
||||||
var frameInfo = await ExtractFrameInfoAsync(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");
|
_logger.LogInformation("Preparing map container for screenshot");
|
||||||
var mapContainer = page.Locator(".esri-view-surface").First;
|
var mapContainer = page.Locator(".esri-view-surface").First;
|
||||||
await mapContainer.WaitForAsync(new LocatorWaitForOptions { Timeout = 10000 });
|
await mapContainer.WaitForAsync(new LocatorWaitForOptions { Timeout = 10000 });
|
||||||
@@ -445,11 +451,11 @@ public class ScrapingService : IScrapingService
|
|||||||
Height = boundingBox.Height
|
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);
|
Directory.CreateDirectory(cacheFolderPath);
|
||||||
_logger.LogInformation("Using cache folder: {Path}", 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 frames = new List<RadarFrame>();
|
||||||
var stepForwardButton = page.Locator("button[data-testid='bom-scrub-utils__right__step-forward']").First;
|
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);
|
frameIndex, framePath, minutesAgo.Value);
|
||||||
|
|
||||||
// Save debug screenshot BEFORE clicking step forward
|
// 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 not the last frame, click step forward to prepare for next frame
|
||||||
if (frameIndex < 6)
|
if (frameIndex < 6)
|
||||||
@@ -521,26 +527,11 @@ public class ScrapingService : IScrapingService
|
|||||||
|
|
||||||
_logger.LogInformation("All 7 frames captured successfully");
|
_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.SaveMetadataAsync(cacheFolderPath, lastUpdatedInfo, cancellationToken);
|
||||||
await _cacheService.SaveFramesMetadataAsync(cacheFolderPath, frames, 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
|
// Note: ScrapingService doesn't have access to cache management check interval
|
||||||
// Default to 5 minutes (standard 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);
|
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;
|
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
|
await page.ScreenshotAsync(new PageScreenshotOptions
|
||||||
{
|
{
|
||||||
Path = outputPath,
|
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})",
|
_logger.LogDebug("Screenshot saved: {Path} (crop: {X},{Y} {Width}x{Height})",
|
||||||
|
|||||||
@@ -80,13 +80,18 @@
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
aspect-ratio: 16/9;
|
aspect-ratio: 16/9;
|
||||||
|
min-height: 400px; /* Prevent layout shift */
|
||||||
}
|
}
|
||||||
|
|
||||||
.radar-image {
|
.radar-image {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
object-fit: contain;
|
object-fit: contain;
|
||||||
display: block;
|
display: block;
|
||||||
|
image-rendering: crisp-edges; /* Prevent blurry scaling */
|
||||||
}
|
}
|
||||||
|
|
||||||
.loading {
|
.loading {
|
||||||
@@ -732,6 +737,9 @@
|
|||||||
</button>
|
</button>
|
||||||
`).join('');
|
`).join('');
|
||||||
|
|
||||||
|
// Preload all frame images to prevent jiggle when switching
|
||||||
|
preloadImages(frames);
|
||||||
|
|
||||||
// Show first frame
|
// Show first frame
|
||||||
if (frames.length > 0) {
|
if (frames.length > 0) {
|
||||||
showFrame(0);
|
showFrame(0);
|
||||||
@@ -745,6 +753,14 @@
|
|||||||
updateRelativeTimes();
|
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
|
// Show specific frame
|
||||||
function showFrame(index) {
|
function showFrame(index) {
|
||||||
if (index < 0 || index >= frames.length) return;
|
if (index < 0 || index >= frames.length) return;
|
||||||
@@ -756,13 +772,21 @@
|
|||||||
const imgEl = document.getElementById('radar-image');
|
const imgEl = document.getElementById('radar-image');
|
||||||
const loadingEl = document.getElementById('loading');
|
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';
|
loadingEl.style.display = 'none';
|
||||||
imgEl.style.display = 'block';
|
imgEl.style.display = 'block';
|
||||||
};
|
};
|
||||||
|
preloadedImg.onerror = () => {
|
||||||
imgEl.src = frame.imageUrl;
|
// Fallback if preload failed
|
||||||
imgEl.alt = `Radar frame ${frame.frameIndex} (${frame.minutesAgo} minutes ago)`;
|
imgEl.src = frame.imageUrl;
|
||||||
|
imgEl.alt = `Radar frame ${frame.frameIndex} (${frame.minutesAgo} minutes ago)`;
|
||||||
|
};
|
||||||
|
preloadedImg.src = frame.imageUrl;
|
||||||
|
|
||||||
// Update frame info
|
// Update frame info
|
||||||
document.getElementById('frame-info').textContent =
|
document.getElementById('frame-info').textContent =
|
||||||
|
|||||||
+1
-1
@@ -29,7 +29,7 @@ services:
|
|||||||
- CACHECLEANUP__INTERVALHOURS=${CACHECLEANUP__INTERVALHOURS:-1}
|
- CACHECLEANUP__INTERVALHOURS=${CACHECLEANUP__INTERVALHOURS:-1}
|
||||||
- SCREENSHOT__DYNAMICCONTENTWAITMS=${SCREENSHOT__DYNAMICCONTENTWAITMS:-2000}
|
- SCREENSHOT__DYNAMICCONTENTWAITMS=${SCREENSHOT__DYNAMICCONTENTWAITMS:-2000}
|
||||||
- SCREENSHOT__TILERENDERWAITMS=${SCREENSHOT__TILERENDERWAITMS:-5000}
|
- SCREENSHOT__TILERENDERWAITMS=${SCREENSHOT__TILERENDERWAITMS:-5000}
|
||||||
- DEBUG__ENABLED=${DEBUG__ENABLED:-false}
|
- DEBUG__ENABLED=${DEBUG__ENABLED:-true}
|
||||||
- DEBUG__WAITMS=${DEBUG__WAITMS:-2000}
|
- DEBUG__WAITMS=${DEBUG__WAITMS:-2000}
|
||||||
- TIMEZONE=${TIMEZONE:-Australia/Brisbane}
|
- TIMEZONE=${TIMEZONE:-Australia/Brisbane}
|
||||||
shm_size: '1gb'
|
shm_size: '1gb'
|
||||||
|
|||||||
Reference in New Issue
Block a user