PostHog / PostHog/posthog-android
Session replay: support opt-in density-aware screenshot sizing
Nobody has claimed this yet.
- Dominant language
- Kotlin
- Stars
- 90
- Forks
- 49
- Avg merge
- 2d 5h
- Merged PRs (30d)
- 61
Description
Problem Statement
Track density-aware session replay screenshot sizing as a separate, non-blocking follow-up to #761 and the original suggestion.
At #761 revision defdd171fe7a815d374aca9bf01a5c1d53cfb3af, screenshotScale multiplies the window's physical pixel dimensions. This allows manual downscaling, but a fixed scale captures more pixels on higher-density displays even when the logical UI size is similar.
For example, a 1080-pixel-wide window at density 3 is captured at 540 pixels with screenshotScale = 0.5f. A density-aware target of approximately one bitmap pixel per dp would capture it at 360 pixels instead. Reducing the bitmap dimensions can reduce capture-buffer memory and pixels processed during masking/encoding, at the cost of image detail. CPU, latency, and payload improvements need measurement rather than assumptions.
This concerns the PixelCopy destination bitmap resolution, not the existing density conversion of logical replay coordinates. It is separate from capture scheduling/backpressure work in #764.
Solution Brainstorm
Opt-in sizing policy
Consider an explicit density-aware resolution mode, or a future quality preset that selects it. Keep resolution policy independent of compression quality, color mode, and capture frequency. Bitmap reuse should remain internal.
One possible API shape, illustrative and not currently available, is:
sessionReplayConfig.apply {
screenshot = true
screenshotResolutionMode = PostHogScreenshotResolutionMode.DENSITY_INDEPENDENT
screenshotScale = 1f // Approximately one bitmap pixel per dp in this mode.
screenshotCompressionQuality = 30
screenshotColorMode = PostHogScreenshotColorMode.ARGB_8888
throttleDelayMs = 1_000
}
Keep PHYSICAL_PIXELS as the default resolution mode, with existing scale semantics, scale 1f, compression quality 30, and ARGB_8888. Do not silently change existing consumers' capture fidelity. Add any new config properties in the class body to preserve constructor signatures. The final API shape remains open for discussion.
Illustrative sizing logic
For a valid positive source size and display density, sample the source dimensions, configuration, and the captured view's display density once per capture:
val baseScale = if (densityAware) 1f / density else 1f
val effectiveScale = minOf(1f, baseScale * screenshotScale)
val captureWidth = maxOf(1, ceil(sourceWidth * effectiveScale).toInt())
val captureHeight = maxOf(1, ceil(sourceHeight * effectiveScale).toInt())
Clamp the final scale to avoid upscaling low-density displays, and retain at least one pixel per dimension. Do not independently density-scale the logical viewport again. WebP screenshots do not need video-codec block alignment.
Tradeoffs and acceptance criteria
- Preserve existing defaults and document physical-pixel versus density-independent sizing, including low-density behavior and the detail/readability tradeoff.
- Derive mask X/Y scales from the actual rounded bitmap dimensions divided by the captured source dimensions. Preserve outward rounding of mask bounds and proportional corner radii; do not weaken mask-alignment guards to improve capture throughput.
- Test legacy and verified masking paths, Compose, odd source sizes, fractional display densities, tiny masked elements, and resize/orientation/density changes while a capture is pending. Discard captures if pixel/mask alignment cannot be established.
- Keep color/alpha independent: downscaled ARGB_8888 must preserve transparency, while RGB_565 remains an explicit fidelity tradeoff with its existing fallback.
- Ensure pending PixelCopy buffers are never reconfigured or recycled while in use, including when settings or display configuration change.
- Benchmark representative real devices/densities: capture latency, encoding time, allocation and peak memory, dropped frames, and payload size. Check small text, thin borders, icons, and zoomed replay playback visually before choosing or recommending preset values.
No physical-device benchmark or measured performance improvement is claimed by this proposal.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with the PixelCopy destination bitmap sizing and screenshot configuration paths introduced by #761, keeping them separate from logical replay-coordinate density conversion and capture scheduling. Define and document the opt-in policy while preserving physical-pixel defaults, then validate masking, configuration changes, color handling, and representative device performance against the listed acceptance criteria.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- android, kotlin
- Domain
- mobile-dev
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 45/100